diff --git a/.gitignore b/.gitignore index e8bcaa8..9fb6965 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ cmake-build-release /.gdb_history /valgrind.log .idea +/perf.data +/perf.data.old +/clusters.puzzle diff --git a/benchmark/state_space.cpp b/benchmark/state_space.cpp index 80e9ca7..62d8ec7 100644 --- a/benchmark/state_space.cpp +++ b/benchmark/state_space.cpp @@ -46,6 +46,46 @@ static auto explore_state_space(benchmark::State& state) -> void } } +static auto explore_rush_hour_puzzle_space(benchmark::State& state) -> void +{ + // ReSharper disable once CppTooWideScope + constexpr uint8_t 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 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 puzzle::block target_block = puzzle::block(0, 0, 2, 1, true, false); + constexpr std::tuple target_block_pos_range = { + 0, + goal_y, + board_width - 1, + goal_y + }; + + 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( + permitted_blocks, + target_block, + target_block_pos_range, + max_blocks, + std::nullopt); + + benchmark::DoNotOptimize(result); + } +} + BENCHMARK(explore_state_space)->DenseRange(0, puzzles.size() - 1)->Unit(benchmark::kMicrosecond); +BENCHMARK(explore_rush_hour_puzzle_space)->Unit(benchmark::kSecond); BENCHMARK_MAIN(); \ No newline at end of file diff --git a/include/config.hpp b/include/config.hpp index 47897e6..c718a48 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -5,6 +5,17 @@ #define THREADPOOL // Enable physics threadpool +// TODO: Using the octree from the last frame completely breaks the physics :/ +// #define ASYNC_OCTREE + +// Gets set by CMake +// #define BACKWARD // Enable pretty stack traces +// #define TRACY // Enable tracy profiling support + +#ifdef TRACY +#include +#endif + #ifdef THREADPOOL #if defined(_WIN32) #define NOGDI // All GDI defines and routines @@ -19,13 +30,6 @@ #endif #endif -// TODO: Using the octree from the last frame completely breaks the physics :/ -// #define ASYNC_OCTREE - -// Gets set by CMake -// #define BACKWARD // Enable pretty stack traces -// #define TRACY // Enable tracy profiling support - // Window constexpr int INITIAL_WIDTH = 600; constexpr int INITIAL_HEIGHT = 600; diff --git a/include/graph_distances.hpp b/include/graph_distances.hpp index 5040903..63df736 100644 --- a/include/graph_distances.hpp +++ b/include/graph_distances.hpp @@ -21,4 +21,4 @@ public: [[nodiscard]] auto get_shortest_path(size_t source) const -> std::vector; }; -#endif +#endif \ No newline at end of file diff --git a/include/input_handler.hpp b/include/input_handler.hpp index 8583394..b852b63 100644 --- a/include/input_handler.hpp +++ b/include/input_handler.hpp @@ -1,5 +1,5 @@ -#ifndef INPUT_HPP_ -#define INPUT_HPP_ +#ifndef INPUT_HANDLER_HPP_ +#define INPUT_HANDLER_HPP_ #include "orbit_camera.hpp" #include "state_manager.hpp" diff --git a/include/load_save.hpp b/include/load_save.hpp new file mode 100644 index 0000000..cde7123 --- /dev/null +++ b/include/load_save.hpp @@ -0,0 +1,15 @@ +#ifndef LOAD_SAVE_HPP_ +#define LOAD_SAVE_HPP_ + +#include "puzzle.hpp" + +#include + +auto parse_preset_file(const std::string& preset_file) -> std::pair, std::vector>; +auto append_preset_file(const std::string& preset_file, const std::string& preset_name, const puzzle& p) -> bool; +auto append_preset_file_quiet(const std::string& preset_file, + const std::string& preset_name, + const puzzle& p, + bool validate) -> bool; + +#endif \ No newline at end of file diff --git a/include/mass_spring_system.hpp b/include/mass_spring_system.hpp index db77ee6..58e8da3 100644 --- a/include/mass_spring_system.hpp +++ b/include/mass_spring_system.hpp @@ -4,6 +4,7 @@ #include "octree.hpp" #include "config.hpp" +#include #include class mass_spring_system @@ -20,11 +21,6 @@ public: : a(_a), b(_b) {} }; -private: - #ifdef THREADPOOL - BS::thread_pool<> threads; - #endif - public: static constexpr int SMALL_TASK_BLOCK_SIZE = 256; static constexpr int LARGE_TASK_BLOCK_SIZE = 256; @@ -40,22 +36,13 @@ public: std::vector springs; public: - mass_spring_system() - #ifdef THREADPOOL - : threads(std::thread::hardware_concurrency() - 2, set_mass_springs_pool_thread_name) - #endif - {} + mass_spring_system() {} mass_spring_system(const mass_spring_system& copy) = delete; auto operator=(const mass_spring_system& copy) -> mass_spring_system& = delete; mass_spring_system(mass_spring_system& move) = delete; auto operator=(mass_spring_system&& move) -> mass_spring_system& = delete; -private: - #ifdef THREADPOOL - static auto set_mass_springs_pool_thread_name(size_t idx) -> void; - #endif - public: auto clear() -> void; auto add_mass() -> void; @@ -63,14 +50,14 @@ public: auto clear_forces() -> void; auto calculate_spring_force(size_t s) -> void; - auto calculate_spring_forces() -> void; - auto calculate_repulsion_forces() -> 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 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) -> void; + auto update(float dt, std::optional* const> thread_pool = std::nullopt) -> void; - auto center_masses() -> void; + auto center_masses(std::optional* const> thread_pool = std::nullopt) -> void; }; #endif \ No newline at end of file diff --git a/include/octree.hpp b/include/octree.hpp index defdf27..2b569ef 100644 --- a/include/octree.hpp +++ b/include/octree.hpp @@ -2,9 +2,10 @@ #define OCTREE_HPP_ #include +#include + #include #include -#include class octree { diff --git a/include/orbit_camera.hpp b/include/orbit_camera.hpp index 68efdfd..9492d72 100644 --- a/include/orbit_camera.hpp +++ b/include/orbit_camera.hpp @@ -28,4 +28,4 @@ public: bool mass_center_lock) -> void; }; -#endif +#endif \ No newline at end of file diff --git a/include/puzzle.hpp b/include/puzzle.hpp index b090446..7be43c3 100644 --- a/include/puzzle.hpp +++ b/include/puzzle.hpp @@ -1,16 +1,24 @@ #ifndef PUZZLE_HPP_ #define PUZZLE_HPP_ +#include "config.hpp" #include "util.hpp" #include #include -#include #include #include #include #include -#include +#include + +// #define RUNTIME_CHECKS + +// Forward declare to use in puzzle member functions +struct block_hasher; +struct block_hasher2; +struct block_equal2; +struct puzzle_hasher; /* * 8x8 board @@ -125,29 +133,33 @@ public: } private: - [[nodiscard]] static auto create_repr(uint8_t x, uint8_t y, uint8_t w, uint8_t h, bool t = false, - bool i = false) -> uint16_t; - // Repr setters - [[nodiscard]] auto set_x(uint8_t x) const -> block; - [[nodiscard]] auto set_y(uint8_t y) const -> block; - [[nodiscard]] auto set_width(uint8_t width) const -> block; - [[nodiscard]] auto set_height(uint8_t height) const -> block; - [[nodiscard]] auto set_target(bool target) const -> block; - [[nodiscard]] auto set_immovable(bool immovable) const -> block; + [[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]] inline auto set_target(bool target) const -> block; + [[nodiscard]] inline auto set_immovable(bool immovable) const -> block; public: - [[nodiscard]] auto unpack_repr() const -> std::tuple; - // Repr getters - [[nodiscard]] auto get_x() const -> uint8_t; - [[nodiscard]] auto get_y() const -> uint8_t; - [[nodiscard]] auto get_width() const -> uint8_t; - [[nodiscard]] auto get_height() const -> uint8_t; - [[nodiscard]] auto get_target() const -> bool; - [[nodiscard]] auto get_immovable() const -> bool; + [[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]] inline auto get_target() const -> bool; + [[nodiscard]] inline auto get_immovable() const -> bool; // Util + [[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 covers(int _x, int _y) const -> bool; @@ -199,7 +211,8 @@ private: // repr_cooked() = delete; // repr_cooked(const repr_cooked& copy) = delete; // repr_cooked(repr_cooked&& move) = delete; - } __attribute__((packed)); + } + PACKED; /** * With gcc, were allowed to acces the members arbitrarily, even if they're not active (not the ones last written): @@ -247,7 +260,12 @@ public: } } - 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 uint8_t w, + const uint8_t h, + const uint8_t tx, + const uint8_t ty, + const bool r, + const bool g, const std::array& b) : repr(create_repr(w, h, tx, ty, r, g, b)) { @@ -259,6 +277,14 @@ public: } } + puzzle(const uint8_t w, const uint8_t 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) { + throw std::invalid_argument("Board size out of bounds"); + } + } + explicit puzzle(const std::string& string_repr) : repr(create_repr(string_repr)) {} @@ -331,33 +357,38 @@ private: return blocks; } - [[nodiscard]] static auto create_meta(const std::tuple& meta) -> uint16_t; - [[nodiscard]] static auto create_repr(uint8_t w, uint8_t h, uint8_t tx, uint8_t ty, bool r, bool g, - const std::array& b) -> repr_cooked; - - [[nodiscard]] static auto create_repr(uint64_t byte_0, uint64_t byte_1, uint64_t byte_2, - uint64_t byte_3) -> repr_cooked; - - [[nodiscard]] static auto create_repr(const std::string& string_repr) -> repr_cooked; - // Repr setters - [[nodiscard]] auto set_restricted(bool restricted) const -> puzzle; - [[nodiscard]] auto set_width(uint8_t width) const -> puzzle; - [[nodiscard]] auto set_height(uint8_t height) const -> puzzle; - [[nodiscard]] auto set_goal(bool goal) const -> puzzle; - [[nodiscard]] auto set_goal_x(uint8_t target_x) const -> puzzle; - [[nodiscard]] auto set_goal_y(uint8_t target_y) const -> puzzle; + [[nodiscard]] static auto create_meta( + const std::tuple& meta) -> uint16_t; + [[nodiscard]] static auto create_repr(uint8_t w, + uint8_t h, + uint8_t tx, + uint8_t ty, + bool r, + bool g, + const std::array& b) -> repr_cooked; + [[nodiscard]] static auto create_repr(uint64_t byte_0, + uint64_t byte_1, + uint64_t byte_2, + uint64_t byte_3) -> repr_cooked; + [[nodiscard]] static auto create_repr(const std::string& string_repr) -> repr_cooked; + [[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_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; public: // Repr getters [[nodiscard]] auto unpack_meta() const -> std::tuple; - [[nodiscard]] auto get_restricted() const -> bool; - [[nodiscard]] auto get_width() const -> uint8_t; - [[nodiscard]] auto get_height() const -> uint8_t; - [[nodiscard]] auto get_goal() const -> bool; - [[nodiscard]] auto get_goal_x() const -> uint8_t; - [[nodiscard]] auto get_goal_y() const -> uint8_t; + [[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_goal() const -> bool; + [[nodiscard]] inline auto get_goal_x() const -> uint8_t; + [[nodiscard]] inline auto get_goal_y() const -> uint8_t; // Util [[nodiscard]] auto hash() const -> size_t; @@ -390,19 +421,46 @@ public: [[nodiscard]] auto try_move_block_at(uint8_t x, uint8_t y, direction dir) const -> std::optional; // Statespace - [[nodiscard]] auto try_move_block_at_fast(uint64_t bitmap, uint8_t block_idx, - direction dir) const -> std::optional; - static auto sorted_replace(std::array blocks, uint8_t idx, - uint16_t new_val) -> std::array; - auto blocks_bitmap() const -> uint64_t; - static inline auto bitmap_set_bit(uint64_t bitmap, uint8_t x, uint8_t y) -> uint64_t; - static inline auto bitmap_get_bit(uint64_t bitmap, uint8_t x, uint8_t y) -> bool; - static auto bitmap_clear_block(uint64_t bitmap, block b) -> uint64_t; - static auto bitmap_check_collision(uint64_t bitmap, block b) -> bool; - static auto bitmap_check_collision(uint64_t bitmap, block b, direction dir) -> bool; + [[nodiscard]] INLINE inline auto try_move_block_at_fast(uint64_t bitmap, + uint8_t block_idx, + direction 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]] 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; + + /** + * Checks if b would collide with any block on the board. + * + * @param bitmap Board occupancy map + * @param b Hypothetical block to check collision with + * @return True if b would collide with any other block on the board + */ + [[nodiscard]] INLINE inline auto bitmap_check_collision(uint64_t bitmap, block b) const -> bool; + + /** + * Checks if b would collide with any block on the board after moving in direction dir. + * + * @param bitmap Board occupancy map + * @param b Existing block to check collision with + * @param dir Direction in which the block should be moved + * @return True if b would collide with any other block on the board after moving in direction dir + */ + [[nodiscard]] INLINE inline auto bitmap_check_collision(uint64_t bitmap, block b, direction dir) const -> bool; template - auto for_each_adjacent(F&& callback) const -> void + // 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(); @@ -427,9 +485,444 @@ public: [[nodiscard]] auto explore_state_space() const -> std::pair, std::vector>>; + + // 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 bitmap_find_first_empty(uint64_t bitmap, int& x, int& y) const -> bool; + + 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 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; + + [[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>; }; -// Hash functions for sets and maps +// Inline functions definitions +#ifndef REGION_INLINE_DEFS + +inline auto puzzle::block::set_x(const uint8_t x) const -> block +{ + #ifdef RUNTIME_CHECKS + if (x > 7) { + throw std::invalid_argument("Block x-position out of bounds"); + } + #endif + + block b = *this; + set_bits(b.repr, X_S, X_E, x); + return b; +} + +inline auto puzzle::block::set_y(const uint8_t y) const -> block +{ + #ifdef RUNTIME_CHECKS + if (y > 7) { + throw std::invalid_argument("Block y-position out of bounds"); + } + #endif + + block b = *this; + set_bits(b.repr, Y_S, Y_E, y); + return b; +} + +inline auto puzzle::block::set_width(const uint8_t width) const -> block +{ + #ifdef RUNTIME_CHECKS + if (width - 1 > 7) { + throw std::invalid_argument("Block width out of bounds"); + } + #endif + + block b = *this; + set_bits(b.repr, WIDTH_S, WIDTH_E, width - 1u); + return b; +} + +inline auto puzzle::block::set_height(const uint8_t height) const -> block +{ + #ifdef RUNTIME_CHECKS + if (height - 1 > 7) { + throw std::invalid_argument("Block height out of bounds"); + } + #endif + + block b = *this; + set_bits(b.repr, HEIGHT_S, HEIGHT_E, height - 1u); + return b; +} + +inline auto puzzle::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 +{ + block b = *this; + set_bits(b.repr, IMMOVABLE_S, IMMOVABLE_E, immovable); + return b; +} + +inline auto puzzle::block::get_x() const -> uint8_t +{ + return get_bits(repr, X_S, X_E); +} + +inline auto puzzle::block::get_y() const -> uint8_t +{ + return get_bits(repr, Y_S, Y_E); +} + +inline auto puzzle::block::get_width() const -> uint8_t +{ + return get_bits(repr, WIDTH_S, WIDTH_E) + 1u; +} + +inline auto puzzle::block::get_height() const -> uint8_t +{ + return get_bits(repr, HEIGHT_S, HEIGHT_E) + 1u; +} + +inline auto puzzle::block::get_target() const -> bool +{ + return get_bits(repr, TARGET_S, TARGET_E); +} + +inline auto puzzle::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; + 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 +{ + #ifdef RUNTIME_CHECKS + if (width - 1 > MAX_WIDTH) { + throw "Board width out of bounds"; + } + #endif + + uint16_t 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 +{ + #ifdef RUNTIME_CHECKS + if (height - 1 > MAX_HEIGHT) { + throw "Board height out of bounds"; + } + #endif + + uint16_t 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; + 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 +{ + #ifdef RUNTIME_CHECKS + if (target_x >= MAX_WIDTH) { + throw "Board target x out of bounds"; + } + #endif + + uint16_t 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 +{ + #ifdef RUNTIME_CHECKS + if (target_y >= MAX_HEIGHT) { + throw "Board target y out of bounds"; + } + #endif + + uint16_t meta = repr.cooked.meta; + set_bits(meta, GOAL_Y_S, GOAL_Y_E, target_y); + return puzzle(meta, repr.cooked.blocks); +} + +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 +{ + return get_bits(repr.cooked.meta, WIDTH_S, WIDTH_E) + 1u; +} + +inline auto puzzle::get_height() const -> uint8_t +{ + return get_bits(repr.cooked.meta, HEIGHT_S, HEIGHT_E) + 1u; +} + +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 +{ + return get_bits(repr.cooked.meta, GOAL_X_S, GOAL_X_E); +} + +inline auto puzzle::get_goal_y() const -> uint8_t +{ + return get_bits(repr.cooked.meta, GOAL_Y_S, GOAL_Y_E); +} + +INLINE inline auto puzzle::try_move_block_at_fast(uint64_t bitmap, + const uint8_t block_idx, + const direction dir, + const bool check_collision) const -> std::optional +{ + const block b = block(repr.cooked.blocks[block_idx]); + const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr(); + if (bi) { + return std::nullopt; + } + + const auto [w, h, gx, gy, r, g] = unpack_meta(); + const int dirs = r ? b.principal_dirs() : nor | eas | sou | wes; + + // Get target block + int _target_x = bx; + int _target_y = by; + switch (dir) { + case nor: + if (!(dirs & nor) || _target_y < 1) { + return std::nullopt; + } + --_target_y; + break; + case eas: + if (!(dirs & eas) || _target_x + bw >= w) { + return std::nullopt; + } + ++_target_x; + break; + case sou: + if (!(dirs & sou) || _target_y + bh >= h) { + return std::nullopt; + } + ++_target_y; + break; + case wes: + if (!(dirs & wes) || _target_x < 1) { + return std::nullopt; + } + --_target_x; + break; + } + + // Check collisions + if (check_collision) { + bitmap_clear_block(bitmap, b); + if (bitmap_check_collision(bitmap, b, dir)) { + return std::nullopt; + } + } + + // Replace block + const std::array blocks = sorted_replace(repr.cooked.blocks, + block_idx, + block::create_repr( + _target_x, + _target_y, + bw, + bh, + bt)); + + // This constructor doesn't sort + return puzzle(std::make_tuple(w, h, gx, gy, r, g), blocks); +} + +INLINE inline auto puzzle::bitmap_clear_bit(uint64_t& bitmap, const uint8_t w, const uint8_t x, const uint8_t 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 +{ + 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 +{ + 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 +{ + const auto [x, y, w, h, t, i] = b.unpack_repr(); + const uint8_t width = get_width(); + + for (int dy = 0; dy < h; ++dy) { + for (int dx = 0; dx < w; ++dx) { + bitmap_clear_bit(bitmap, width, x + dx, y + dy); + } + } +} + +INLINE inline auto puzzle::bitmap_set_block(uint64_t& bitmap, const block b) const -> void +{ + const auto [x, y, w, h, t, i] = b.unpack_repr(); + const uint8_t width = get_width(); + + for (int dy = 0; dy < h; ++dy) { + for (int dx = 0; dx < w; ++dx) { + bitmap_set_bit(bitmap, width, x + dx, y + dy); + } + } +} + +INLINE inline auto puzzle::bitmap_is_empty(const uint64_t bitmap) const -> bool +{ + const uint8_t shift = 64 - get_width() * get_height(); + return bitmap << shift == 0; +} + +INLINE inline auto puzzle::bitmap_is_full(const uint64_t bitmap) const -> bool +{ + const uint8_t 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 +{ + const auto [x, y, w, h, t, i] = b.unpack_repr(); + const uint8_t width = get_width(); + + for (int dy = 0; dy < h; ++dy) { + for (int dx = 0; dx < w; ++dx) { + if (bitmap_get_bit(bitmap, width, x + dx, y + dy)) { + return true; // collision + } + } + } + + return false; +} + +INLINE inline auto puzzle::bitmap_check_collision(const uint64_t bitmap, + const block b, + const direction dir) const -> bool +{ + const auto [x, y, w, h, t, i] = b.unpack_repr(); + const uint8_t width = get_width(); + + switch (dir) { + case nor: // Check the row above: (x...x+w-1, y-1) + for (int dx = 0; dx < w; ++dx) { + if (bitmap_get_bit(bitmap, width, x + dx, y - 1)) { + return true; + } + } + break; + case sou: // Check the row below: (x...x+w-1, y+h) + for (int dx = 0; dx < w; ++dx) { + if (bitmap_get_bit(bitmap, width, x + dx, y + h)) { + return true; + } + } + break; + case wes: // Check the column left: (x-1, y...y+h-1) + for (int dy = 0; dy < h; ++dy) { + if (bitmap_get_bit(bitmap, width, x - 1, y + dy)) { + return true; + } + } + break; + case eas: // Check the column right: (x+w, y...y+h-1) + for (int dy = 0; dy < h; ++dy) { + if (bitmap_get_bit(bitmap, width, x + w, y + dy)) { + return true; + } + } + break; + } + return false; +} + +#endif + +// Hash functions for sets and maps. +// Declared after puzzle class to use puzzle::hash_combine +#ifndef REGION_HASHERS + +struct block_hasher +{ + auto operator()(const puzzle::block& b) const noexcept -> size_t + { + return b.hash(); + } +}; + +struct block_hasher2 +{ + auto operator()(const puzzle::block& b) const noexcept -> size_t + { + return b.position_independent_hash(); + } +}; + +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; + } +}; struct puzzle_hasher { @@ -453,7 +946,7 @@ struct link_hasher } }; -struct link_equal_to +struct link_equal { auto operator()(const std::pair& a, const std::pair& b) const noexcept -> bool { @@ -462,9 +955,9 @@ struct link_equal_to }; template -auto puzzle::hash_combine(std::size_t& seed, const T& v, const Rest&... rest) -> void +auto puzzle::hash_combine(size_t& seed, const T& v, const Rest&... rest) -> void { - auto h = [](const HashedType& val) -> std::size_t + auto hasher = [](const HashedType& val) -> std::size_t { if constexpr (std::is_same_v, puzzle>) { return puzzle_hasher{}(val); @@ -475,8 +968,10 @@ auto puzzle::hash_combine(std::size_t& seed, const T& v, const Rest&... rest) -> } }; - seed ^= h(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); + seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); (hash_combine(seed, rest), ...); } +#endif + #endif \ No newline at end of file diff --git a/include/renderer.hpp b/include/renderer.hpp index e444b73..801c988 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -1,8 +1,8 @@ #ifndef RENDERER_HPP_ #define RENDERER_HPP_ -#include "orbit_camera.hpp" #include "config.hpp" +#include "orbit_camera.hpp" #include "input_handler.hpp" #include "state_manager.hpp" #include "user_interface.hpp" @@ -18,7 +18,7 @@ private: user_interface& gui; const orbit_camera& camera; - RenderTexture render_target = + RenderTexture graph_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); // TODO: Those should be moved to the user_interface.h @@ -81,7 +81,7 @@ public: ~renderer() { - UnloadRenderTexture(render_target); + UnloadRenderTexture(graph_target); UnloadRenderTexture(klotski_target); UnloadRenderTexture(menu_target); diff --git a/include/state_manager.hpp b/include/state_manager.hpp index 3b3bf50..7ce8954 100644 --- a/include/state_manager.hpp +++ b/include/state_manager.hpp @@ -2,10 +2,10 @@ #define STATE_MANAGER_HPP_ #include "graph_distances.hpp" +#include "load_save.hpp" #include "threaded_physics.hpp" #include "puzzle.hpp" -#include #include #include @@ -43,10 +43,9 @@ private: public: state_manager(threaded_physics& _physics, const std::string& _preset_file) - : physics(_physics) + : physics(_physics), preset_file(_preset_file) { - parse_preset_file(_preset_file); - load_preset(0); + reload_preset_file(); } state_manager(const state_manager& copy) = delete; @@ -95,15 +94,13 @@ private: public: // Presets - - auto parse_preset_file(const std::string& _preset_file) -> bool; - auto append_preset_file(const std::string& preset_name) -> bool; + auto save_current_to_preset_file(const std::string& preset_comment) -> void; + auto reload_preset_file() -> void; auto load_preset(size_t preset) -> void; auto load_previous_preset() -> void; auto load_next_preset() -> void; // Update current_state - auto update_current_state(const puzzle& p) -> void; auto edit_starting_state(const puzzle& p) -> void; auto goto_starting_state() -> void; @@ -113,7 +110,6 @@ public: auto goto_closest_target_state() -> void; // Update graph - auto populate_graph() -> void; auto clear_graph_and_add_current(const puzzle& p) -> void; auto clear_graph_and_add_current() -> void; @@ -122,7 +118,6 @@ public: auto populate_winning_path() -> void; // Index mapping - [[nodiscard]] auto get_index(const puzzle& state) const -> size_t; [[nodiscard]] auto get_current_index() const -> size_t; [[nodiscard]] auto get_starting_index() const -> size_t; diff --git a/include/threaded_physics.hpp b/include/threaded_physics.hpp index de9fb56..c1a03d7 100644 --- a/include/threaded_physics.hpp +++ b/include/threaded_physics.hpp @@ -13,10 +13,6 @@ #include #include -#ifdef TRACY -#include -#endif - class threaded_physics { struct add_mass {}; @@ -65,8 +61,8 @@ public: physics_state state; public: - threaded_physics() - : physics(physics_thread, std::ref(state)) {} + explicit threaded_physics(const std::optional* const> thread_pool = std::nullopt) + : physics(physics_thread, std::ref(state), std::ref(thread_pool)) {} threaded_physics(const threaded_physics& copy) = delete; auto operator=(const threaded_physics& copy) -> threaded_physics& = delete; @@ -86,7 +82,7 @@ private: static auto set_octree_pool_thread_name(size_t idx) -> void; #endif - static auto physics_thread(physics_state& state) -> void; + static auto physics_thread(physics_state& state, std::optional* const> thread_pool) -> void; public: auto clear_cmd() -> void; diff --git a/include/user_interface.hpp b/include/user_interface.hpp index 50c7bac..84f77a4 100644 --- a/include/user_interface.hpp +++ b/include/user_interface.hpp @@ -1,5 +1,5 @@ -#ifndef GUI_HPP_ -#define GUI_HPP_ +#ifndef USER_INTERFACE_HPP_ +#define USER_INTERFACE_HPP_ #include "orbit_camera.hpp" #include "config.hpp" @@ -104,7 +104,7 @@ private: bool ok_message = false; bool yes_no_message = false; bool save_window = false; - std::array preset_name = {}; + std::array preset_comment = {}; bool help_window = false; public: diff --git a/include/util.hpp b/include/util.hpp index 1813287..a5469d3 100644 --- a/include/util.hpp +++ b/include/util.hpp @@ -4,81 +4,8 @@ #include #include -// Bit shifting + masking - -template - requires std::unsigned_integral -auto create_mask(const uint8_t first, const uint8_t 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. - if (static_cast(last - first + 1) >= sizeof(T) * 8) { - return ~T{0}; - } - - // Example: first=4, last=7, 7-4+1=4 - // 1 << 4 = 0b00010000 - // 32 - 1 = 0b00001111 - // 31 << 4 = 0b11110000 - // Subtracting 1 generates a consecutive mask. - return ((T{1} << (last - first + 1)) - 1) << first; -} - -template - requires std::unsigned_integral -auto clear_bits(T& bits, const uint8_t first, const uint8_t last) -> void -{ - const T mask = create_mask(first, last); - - bits = bits & ~mask; -} - -template - requires std::unsigned_integral && std::unsigned_integral -auto set_bits(T& bits, const uint8_t first, const uint8_t last, const U value) -> void -{ - const T mask = create_mask(first, last); - - // Example: first=4, last=6, value=0b1110, bits = 0b 01111110 - // mask = 0b 01110000 - // bits & ~mask = 0b 00001110 - // value << 4 = 0b 11100000 - // (value << 4) & mask = 0b 01100000 - // (bits & ~mask) | (value << 4) & mask = 0b 01101110 - // Insert position: ^^^ - // First clear the bits, then | with the value positioned at the insertion point. - // The value may be larger than [first, last], extra bits are ignored. - bits = (bits & ~mask) | ((static_cast(value) << first) & mask); -} - -template - requires std::unsigned_integral -auto get_bits(const T bits, const uint8_t first, const uint8_t last) -> T -{ - const T mask = create_mask(first, last); - - // We can >> without sign extension because T is unsigned_integral - return (bits & mask) >> first; -} - -// std::variant visitor - -// https://en.cppreference.com/w/cpp/utility/variant/visit -template -struct overloads : Ts... -{ - using Ts::operator()...; -}; - -// Enums - -enum direction -{ - nor = 1 << 0, - eas = 1 << 1, - sou = 1 << 2, - wes = 1 << 3, -}; +#define INLINE __attribute__((always_inline)) +#define PACKED __attribute__((packed)) enum ctrl { @@ -115,6 +42,96 @@ enum bg bg_white = 47 }; +inline auto ansi_bold_fg(const fg color) -> std::string +{ + return std::format("\033[{};{}m", static_cast(bold_bright), static_cast(color)); +} + +inline auto ansi_reset() -> std::string +{ + return std::format("\033[{}m", static_cast(reset)); +} + +// Bit shifting + masking + +template + requires std::unsigned_integral +// ReSharper disable once CppRedundantInlineSpecifier +INLINE inline auto create_mask(const uint8_t first, const uint8_t 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. + if (static_cast(last - first + 1) >= sizeof(T) * 8) { + return ~T{0}; + } + + // Example: first=4, last=7, 7-4+1=4 + // 1 << 4 = 0b00010000 + // 32 - 1 = 0b00001111 + // 31 << 4 = 0b11110000 + // Subtracting 1 generates a consecutive mask. + return ((T{1} << (last - first + 1)) - 1) << first; +} + +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 +{ + const T mask = create_mask(first, last); + + bits = bits & ~mask; +} + +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 +{ + const T mask = create_mask(first, last); + + // Example: first=4, last=6, value=0b1110, bits = 0b 01111110 + // mask = 0b 01110000 + // bits & ~mask = 0b 00001110 + // value << 4 = 0b 11100000 + // (value << 4) & mask = 0b 01100000 + // (bits & ~mask) | (value << 4) & mask = 0b 01101110 + // Insert position: ^^^ + // First clear the bits, then | with the value positioned at the insertion point. + // The value may be larger than [first, last], extra bits are ignored. + bits = (bits & ~mask) | ((static_cast(value) << first) & mask); +} + +template + requires std::unsigned_integral +// ReSharper disable once CppRedundantInlineSpecifier +INLINE inline auto get_bits(const T bits, const uint8_t first, const uint8_t last) -> T +{ + const T mask = create_mask(first, last); + + // We can >> without sign extension because T is unsigned_integral + return (bits & mask) >> first; +} + +// std::variant visitor + +// https://en.cppreference.com/w/cpp/utility/variant/visit +template +struct overloads : Ts... +{ + using Ts::operator()...; +}; + +// Enums + +enum direction +{ + nor = 1 << 0, + eas = 1 << 1, + sou = 1 << 2, + wes = 1 << 3, +}; + // Output inline auto operator<<(std::ostream& os, const Vector2& v) -> std::ostream& @@ -129,16 +146,6 @@ inline auto operator<<(std::ostream& os, const Vector3& v) -> std::ostream& return os; } -inline auto ansi_bold_fg(const fg color) -> std::string -{ - return std::format("\033[1;{}m", static_cast(color)); -} - -inline auto ansi_reset() -> std::string -{ - return "\033[0m"; -} - // std::println doesn't work with mingw template auto traceln(std::format_string fmt, Args&&... args) -> void @@ -168,4 +175,19 @@ auto errln(std::format_string fmt, Args&&... args) -> void fmt, std::forward(args)...) << std::endl; } +inline auto print_bitmap(const uint64_t bitmap, const uint8_t w, const uint8_t h, const std::string& title) -> void +{ + traceln("{}:", title); + traceln("{}", std::string(2 * w - 1, '=')); + for (size_t y = 0; y < w; ++y) { + std::cout << " "; + for (size_t x = 0; x < h; ++x) { + std::cout << static_cast(get_bits(bitmap, y * w + x, y * h + x)) << " "; + } + std::cout << "\n"; + } + std::cout << std::flush; + traceln("{}", std::string(2 * w - 1, '=')); +} + #endif \ No newline at end of file diff --git a/src/graph_distances.cpp b/src/graph_distances.cpp index 1e36fbb..129ce3e 100644 --- a/src/graph_distances.cpp +++ b/src/graph_distances.cpp @@ -2,10 +2,6 @@ #include -#ifdef TRACY - #include -#endif - auto graph_distances::clear() -> void { distances.clear(); diff --git a/src/load_save.cpp b/src/load_save.cpp new file mode 100644 index 0000000..8fbc275 --- /dev/null +++ b/src/load_save.cpp @@ -0,0 +1,79 @@ +#include "load_save.hpp" + +#include + +auto parse_preset_file(const std::string& preset_file) -> std::pair, std::vector> +{ + std::fstream file(preset_file, std::ios::in); + if (!file) { + infoln("Preset file \"{}\" couldn't be opened.", preset_file); + return {}; + } + + std::string line; + std::vector comment_lines; + std::vector preset_lines; + while (std::getline(file, line)) { + if (line.starts_with("S")) { + preset_lines.push_back(line); + } else if (line.starts_with("#")) { + comment_lines.push_back(line); + } + } + + if (preset_lines.empty() || comment_lines.size() != preset_lines.size()) { + infoln("Preset file \"{}\" couldn't be opened.", preset_file); + return {}; + } + + std::vector preset_states; + for (const auto& preset : preset_lines) { + // Each char is a bit + const puzzle& p = puzzle(preset); + + if (const std::optional& reason = p.try_get_invalid_reason()) { + infoln("Preset file \"{}\" contained invalid presets: {}", preset_file, *reason); + return {}; + } + preset_states.emplace_back(p); + } + + infoln("Loaded {} presets from \"{}\".", preset_lines.size(), preset_file); + + return {preset_states, comment_lines}; +} + +auto append_preset_file(const std::string& preset_file, const std::string& preset_name, const puzzle& p) -> bool +{ + infoln(R"(Saving preset "{}" to "{}")", preset_name, preset_file); + + if (p.try_get_invalid_reason()) { + return false; + } + + std::fstream file(preset_file, std::ios_base::app | std::ios_base::out); + if (!file) { + infoln("Preset file \"{}\" couldn't be opened.", preset_file); + return false; + } + + file << "\n# " << preset_name << "\n" << p.string_repr() << std::flush; + + return true; +} + +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; + } + + std::fstream file(preset_file, std::ios_base::app | std::ios_base::out); + if (!file) { + return false; + } + + file << "\n# " << preset_name << "\n" << p.string_repr() << std::flush; + + return true; +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index de41a0c..8b13b5c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,68 +4,57 @@ #include "config.hpp" #include "input_handler.hpp" -#include "mass_spring_system.hpp" #include "threaded_physics.hpp" #include "renderer.hpp" #include "state_manager.hpp" #include "user_interface.hpp" -#ifdef TRACY -#include -#endif +#include +#include -// TODO: Add state space generation time to debug overlay +namespace po = boost::program_options; // 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 - +// TODO: Export cluster to graphviz +// TODO: Fix naming: +// - Target: The block that has to leave the board to win +// - Goal: The opening in the board for the target +// - Puzzle (not board or state): A puzzle configuration (width, height, goal_x, goal_y, restricted, goal) +// - Block: A puzzle block (x, y, width, height, target, immovable) +// - Puzzle State: A specific puzzle state (width, height, goal_x, goal_y, restricted, goal, blocks) +// - Cluster: A graph of puzzle states connected by moves, generated from a specific Puzzle State +// - Puzzle Space: A number of Clusters generated from a generic Puzzle +// 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 -// For profiling explore_state_space -auto main2(int argc, char* argv[]) -> int +#ifdef THREADPOOL +auto set_pool_thread_name(size_t idx) -> void { - // Supercompo - const puzzle p = puzzle( - "S:[4x5] G:[1,3] M:[F] B:[{_ 2X2 _ _} {1x1 _ _ 1x1} {1x2 2x1 _ 1x2} {_ 2x1 _ _} {1x1 2x1 _ 1x1}]"); - - for (int i = 0; i < 50; ++i) { - auto space = p.explore_state_space(); - } - - return 0; + BS::this_thread::set_os_thread_name(std::format("worker-{}", idx)); } -auto main(int argc, char* argv[]) -> int +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; +std::string output_file; +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 { - std::string preset_file; - if (argc != 2) { - preset_file = "default.puzzle"; - } else { - preset_file = argv[1]; - } - - #ifdef BACKWARD - infoln("Backward stack-traces enabled."); - #else - infoln("Backward stack-traces disabled."); - #endif - - #ifdef TRACY - infoln("Tracy adapter enabled."); - #else - infoln("Tracy adapter disabled."); - #endif - - #ifdef THREADPOOL - infoln("Thread-pool enabled."); - #else - infoln("Thread-pool disabled."); - #endif - // RayLib window setup SetTraceLogLevel(LOG_ERROR); SetConfigFlags(FLAG_VSYNC_HINT); @@ -75,7 +64,7 @@ auto main(int argc, char* argv[]) -> int InitWindow(INITIAL_WIDTH * 2, INITIAL_HEIGHT + MENU_HEIGHT, "MassSprings"); // Game setup - threaded_physics physics; + threaded_physics physics(thread_pool); state_manager state(physics, preset_file); orbit_camera camera; input_handler input(state, camera); @@ -159,11 +148,197 @@ auto main(int argc, char* argv[]) -> int ++loop_iterations; #ifdef TRACY - FrameMark; FrameMarkEnd("MainThread"); + FrameMark; + FrameMarkEnd("MainThread"); #endif } CloseWindow(); return 0; +} + +auto rush_hour_puzzle_space() -> int +{ + 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 - 1, goal_y}; + + infoln("Exploring Rush-Hour puzzle space:"); + infoln("- Size: {}x{}", board_width, board_height); + infoln("- Goal: {},{}", goal_x, goal_y); + infoln("- Restricted: {}", restricted); + infoln("- Max Blocks: {}", max_blocks); + infoln("- Target: {}x{}", target_block.get_width(), target_block.get_height()); + infoln("- Permitted block sizes:"); + std::cout << " "; + for (const puzzle::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( + permitted_blocks, + target_block, + target_block_pos_range, + max_blocks, + thread_pool); + + 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()); + + 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()); + // } + + size_t i = 0; + size_t success = 0; + std::filesystem::remove(output_file); + for (const puzzle& _p : result_sorted) { + if (append_preset_file_quiet(output_file, std::format("Cluster {}", i), _p, true)) { + ++success; + } + ++i; + } + if (success != result_sorted.size()) { + warnln("Saved {} of {} clusters", success, result_sorted.size()); + } else { + infoln("Saved {} of {} clusters", success, result_sorted.size()); + } + + return 0; +} + +enum class runmode +{ + USER_INTERFACE, RUSH_HOUR_PUZZLE_SPACE, EXIT, +}; + +auto argparse(const int argc, char* argv[]) -> runmode +{ + po::options_description desc("Allowed options"); + desc.add_options() // + ("help", "produce help message") // + + ("presets", po::value()->default_value("default.puzzle"), "load presets from file") // + + ("output", po::value()->default_value("clusters.puzzle"), "output file for generated clusters") // + ("space", po::value()->value_name("rh|klotski"), "generate puzzle space with ruleset") // + ("w", po::value()->default_value(6), "board width") // + ("h", po::value()->default_value(6), "board height") // + ("gx", po::value()->default_value(4), "board goal horizontal position") // + ("gy", po::value()->default_value(2), "board goal vertical position") // + ("free", "allow free block movement") // + ("blocks", po::value()->default_value(5), "block limit for puzzle space generation") // + ; + + po::positional_options_description positional; + positional.add("presets", -1); + + po::variables_map vm; + po::store(po::command_line_parser(argc, argv).options(desc).positional(positional).run(), vm); + po::notify(vm); + + if (vm.contains("help")) { + 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("blocks")) { + max_blocks = vm["blocks"].as(); + max_blocks = std::max(1, std::min(max_blocks, static_cast(puzzle::MAX_BLOCKS))); + } + + 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"); + } + } + + if (vm.contains("presets")) { + preset_file = vm["presets"].as(); + } + + return runmode::USER_INTERFACE; +} + +auto main(const int argc, char* argv[]) -> int +{ + #ifdef BACKWARD + infoln("Backward stack-traces enabled."); + #else + infoln("Backward stack-traces disabled."); + #endif + + #ifdef TRACY + infoln("Tracy adapter enabled."); + #else + infoln("Tracy adapter disabled."); + #endif + + infoln("Using background thread for physics."); + infoln("Using octree-barnes-hut for graph layout."); + + #ifdef THREADPOOL + infoln("Additional thread-pool enabled ({} threads).", threads.get_thread_count()); + #else + infoln("Additional thread-pool disabled."); + #endif + + switch (argparse(argc, argv)) { + case runmode::USER_INTERFACE: + return ui_mode(); + case runmode::RUSH_HOUR_PUZZLE_SPACE: + return rush_hour_puzzle_space(); + case runmode::EXIT: + return 0; + }; + + return 1; } \ No newline at end of file diff --git a/src/mass_spring_system.cpp b/src/mass_spring_system.cpp index 5849c02..54ec19b 100644 --- a/src/mass_spring_system.cpp +++ b/src/mass_spring_system.cpp @@ -1,14 +1,9 @@ #include "mass_spring_system.hpp" #include "config.hpp" -#include "util.hpp" #include #include -#ifdef TRACY -#include -#endif - auto mass_spring_system::calculate_spring_force(const size_t s) -> void { const spring _s = springs[s]; @@ -21,7 +16,7 @@ auto mass_spring_system::calculate_spring_force(const size_t s) -> void const Vector3 delta_vel = a_vel - b_vel; const float sq_len = Vector3DotProduct(delta_pos, delta_pos); - const float inv_len = 1.0f / sqrt(sq_len); + const float inv_len = rsqrt(sq_len); const float len = sq_len * inv_len; const float hooke = SPRING_CONSTANT * (len - REST_LENGTH); @@ -58,14 +53,6 @@ auto mass_spring_system::verlet_update(const size_t m, const float dt) -> void previous_positions[m] = pos; } -#ifdef THREADPOOL -auto mass_spring_system::set_mass_springs_pool_thread_name(size_t idx) -> void -{ - BS::this_thread::set_os_thread_name(std::format("repulsion-{}", idx)); - traceln("Using thread \"{}\"", BS::this_thread::get_os_thread_name().value_or("INVALID NAME")); -} -#endif - auto mass_spring_system::clear() -> void { positions.clear(); @@ -134,7 +121,7 @@ auto mass_spring_system::clear_forces() -> void memset(forces.data(), 0, forces.size() * sizeof(Vector3)); } -auto mass_spring_system::calculate_spring_forces() -> void +auto mass_spring_system::calculate_spring_forces(const std::optional* const> thread_pool) -> void { #ifdef TRACY ZoneScoped; @@ -145,16 +132,16 @@ auto mass_spring_system::calculate_spring_forces() -> void calculate_spring_force(i); }; - #ifdef THREADPOOL - threads.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); + if (thread_pool) { + (*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); + } } - #endif } -auto mass_spring_system::calculate_repulsion_forces() -> void +auto mass_spring_system::calculate_repulsion_forces(const std::optional* const> thread_pool) -> void { #ifdef TRACY ZoneScoped; @@ -167,16 +154,16 @@ auto mass_spring_system::calculate_repulsion_forces() -> void }; // Calculate forces using Barnes-Hut - #ifdef THREADPOOL - threads.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); + if (thread_pool) { + (*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); + } } - #endif } -auto mass_spring_system::update(const float dt) -> void +auto mass_spring_system::update(const float dt, const std::optional* const> thread_pool) -> void { #ifdef TRACY ZoneScoped; @@ -187,16 +174,16 @@ auto mass_spring_system::update(const float dt) -> void verlet_update(i, dt); }; - #ifdef THREADPOOL - threads.submit_loop(0, positions.size(), update, SMALL_TASK_BLOCK_SIZE).wait(); - #else - for (size_t i = 0; i < positions.size(); ++i) { - update(i); + if (thread_pool) { + (*thread_pool)->submit_loop(0, positions.size(), update, SMALL_TASK_BLOCK_SIZE).wait(); + } else { + for (size_t i = 0; i < positions.size(); ++i) { + update(i); + } } - #endif } -auto mass_spring_system::center_masses() -> void +auto mass_spring_system::center_masses(const std::optional* const> thread_pool) -> void { Vector3 mean = Vector3Zero(); for (const Vector3& pos : positions) { @@ -209,11 +196,11 @@ auto mass_spring_system::center_masses() -> void positions[i] -= mean; }; - #ifdef THREADPOOL - threads.submit_loop(0, positions.size(), center_mass, SMALL_TASK_BLOCK_SIZE).wait(); - #else - for (size_t i = 0; i < positions.size(); ++i) { - center_mass(i); + if (thread_pool) { + (*thread_pool)->submit_loop(0, positions.size(), center_mass, SMALL_TASK_BLOCK_SIZE).wait(); + } else { + for (size_t i = 0; i < positions.size(); ++i) { + center_mass(i); + } } - #endif } \ No newline at end of file diff --git a/src/octree.cpp b/src/octree.cpp index 86928c6..b3639a7 100644 --- a/src/octree.cpp +++ b/src/octree.cpp @@ -4,10 +4,6 @@ #include #include -#ifdef TRACY -#include -#endif - auto octree::node::child_count() const -> int { int child_count = 0; diff --git a/src/orbit_camera.cpp b/src/orbit_camera.cpp index d607d3b..6619d6e 100644 --- a/src/orbit_camera.cpp +++ b/src/orbit_camera.cpp @@ -4,10 +4,6 @@ #include #include -#ifdef TRACY - #include -#endif - auto orbit_camera::rotate(const Vector2 last_mouse, const Vector2 mouse) -> void { const auto [dx, dy] = Vector2Subtract(mouse, last_mouse); @@ -73,4 +69,4 @@ auto orbit_camera::update(const Vector3& current_target, const Vector3& mass_cen camera.target = target; camera.fovy = fov; camera.projection = projection; -} +} \ No newline at end of file diff --git a/src/puzzle.cpp b/src/puzzle.cpp index 54d1f54..143b50e 100644 --- a/src/puzzle.cpp +++ b/src/puzzle.cpp @@ -3,70 +3,16 @@ #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, +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 { return block().set_x(x).set_y(y).set_width(w).set_height(h).set_target(t).set_immovable(i).repr & ~INVALID; } -auto puzzle::block::set_x(const uint8_t x) const -> block -{ - if (x > 7) { - throw std::invalid_argument("Block x-position out of bounds"); - } - - block b = *this; - set_bits(b.repr, X_S, X_E, x); - return b; -} - -auto puzzle::block::set_y(const uint8_t y) const -> block -{ - if (y > 7) { - throw std::invalid_argument("Block y-position out of bounds"); - } - - block b = *this; - set_bits(b.repr, Y_S, Y_E, y); - return b; -} - -auto puzzle::block::set_width(const uint8_t width) const -> block -{ - if (width - 1 > 7) { - throw std::invalid_argument("Block width out of bounds"); - } - - block b = *this; - set_bits(b.repr, WIDTH_S, WIDTH_E, width - 1u); - return b; -} - -auto puzzle::block::set_height(const uint8_t height) const -> block -{ - if (height - 1 > 7) { - throw std::invalid_argument("Block height out of bounds"); - } - - block b = *this; - set_bits(b.repr, HEIGHT_S, HEIGHT_E, height - 1u); - return b; -} - -auto puzzle::block::set_target(const bool target) const -> block -{ - block b = *this; - set_bits(b.repr, TARGET_S, TARGET_E, target); - return b; -} - -auto puzzle::block::set_immovable(const bool immovable) const -> block -{ - block b = *this; - set_bits(b.repr, IMMOVABLE_S, IMMOVABLE_E, immovable); - return b; -} - auto puzzle::block::unpack_repr() const -> std::tuple { const uint8_t x = get_x(); @@ -79,46 +25,29 @@ auto puzzle::block::unpack_repr() const -> std::tuple uint8_t +auto puzzle::block::hash() const -> size_t { - return get_bits(repr, X_S, X_E); + return std::hash{}(repr); } -auto puzzle::block::get_y() const -> uint8_t +auto puzzle::block::position_independent_hash() const -> size_t { - return get_bits(repr, Y_S, Y_E); -} - -auto puzzle::block::get_width() const -> uint8_t -{ - return get_bits(repr, WIDTH_S, WIDTH_E) + 1u; -} - -auto puzzle::block::get_height() const -> uint8_t -{ - return get_bits(repr, HEIGHT_S, HEIGHT_E) + 1u; -} - -auto puzzle::block::get_target() const -> bool -{ - return get_bits(repr, TARGET_S, TARGET_E); -} - -auto puzzle::block::get_immovable() const -> bool -{ - return get_bits(repr, IMMOVABLE_S, IMMOVABLE_E); + uint16_t r = repr; + clear_bits(r, X_S, X_E); + clear_bits(r, Y_S, Y_E); + return std::hash{}(r); } auto puzzle::block::valid() const -> bool { - const auto [x, y, w, h, t, i] = unpack_repr(); - - if (t && i) { + // This means the first bit is set, marking the block as empty + if (repr & INVALID) { return false; } - // This means the first bit is set, marking the block as empty - if (repr & INVALID) { + const auto [x, y, w, h, t, i] = unpack_repr(); + + if (t && i) { return false; } @@ -171,8 +100,13 @@ auto puzzle::create_meta(const std::tuple& b) -> repr_cooked +auto puzzle::create_repr(const uint8_t w, + const uint8_t h, + const uint8_t tx, + const uint8_t ty, + const bool r, + const bool g, + const std::array& b) -> repr_cooked { repr_cooked repr = puzzle().set_width(w).set_height(h).set_goal_x(tx).set_goal_y(ty).set_restricted(r).set_goal(g). set_blocks(b).repr.cooked; @@ -180,7 +114,9 @@ auto puzzle::create_repr(const uint8_t w, const uint8_t h, const uint8_t tx, con return repr; } -auto puzzle::create_repr(const uint64_t byte_0, const uint64_t byte_1, const uint64_t byte_2, +auto puzzle::create_repr(const uint64_t byte_0, + const uint64_t byte_1, + const uint64_t byte_2, const uint64_t byte_3) -> repr_cooked { repr_u repr{}; @@ -199,64 +135,6 @@ auto puzzle::create_repr(const std::string& string_repr) -> repr_cooked return *repr; } -auto puzzle::set_restricted(const bool restricted) const -> puzzle -{ - uint16_t meta = repr.cooked.meta; - set_bits(meta, RESTRICTED_S, RESTRICTED_E, restricted); - return puzzle(meta, repr.cooked.blocks); -} - -auto puzzle::set_width(const uint8_t width) const -> puzzle -{ - if (width - 1 > MAX_WIDTH) { - throw "Board width out of bounds"; - } - - uint16_t meta = repr.cooked.meta; - set_bits(meta, WIDTH_S, WIDTH_E, width - 1u); - return puzzle(meta, repr.cooked.blocks); -} - -auto puzzle::set_height(const uint8_t height) const -> puzzle -{ - if (height - 1 > MAX_HEIGHT) { - throw "Board height out of bounds"; - } - - uint16_t meta = repr.cooked.meta; - set_bits(meta, HEIGHT_S, HEIGHT_E, height - 1u); - return puzzle(meta, repr.cooked.blocks); -} - -auto puzzle::set_goal(const bool goal) const -> puzzle -{ - uint16_t meta = repr.cooked.meta; - set_bits(meta, GOAL_S, GOAL_E, goal); - return puzzle(meta, repr.cooked.blocks); -} - -auto puzzle::set_goal_x(const uint8_t target_x) const -> puzzle -{ - if (target_x >= MAX_WIDTH) { - throw "Board target x out of bounds"; - } - - uint16_t meta = repr.cooked.meta; - set_bits(meta, GOAL_X_S, GOAL_X_E, target_x); - return puzzle(meta, repr.cooked.blocks); -} - -auto puzzle::set_goal_y(const uint8_t target_y) const -> puzzle -{ - if (target_y >= MAX_HEIGHT) { - throw "Board target y out of bounds"; - } - - uint16_t meta = repr.cooked.meta; - set_bits(meta, GOAL_Y_S, GOAL_Y_E, target_y); - return puzzle(meta, repr.cooked.blocks); -} - auto puzzle::set_blocks(std::array blocks) const -> puzzle { puzzle p = *this; @@ -277,36 +155,6 @@ auto puzzle::unpack_meta() const -> std::tuple bool -{ - return get_bits(repr.cooked.meta, RESTRICTED_S, RESTRICTED_E); -} - -auto puzzle::get_width() const -> uint8_t -{ - return get_bits(repr.cooked.meta, WIDTH_S, WIDTH_E) + 1u; -} - -auto puzzle::get_height() const -> uint8_t -{ - return get_bits(repr.cooked.meta, HEIGHT_S, HEIGHT_E) + 1u; -} - -auto puzzle::get_goal() const -> bool -{ - return get_bits(repr.cooked.meta, GOAL_S, GOAL_E); -} - -auto puzzle::get_goal_x() const -> uint8_t -{ - return get_bits(repr.cooked.meta, GOAL_X_S, GOAL_X_E); -} - -auto puzzle::get_goal_y() const -> uint8_t -{ - return get_bits(repr.cooked.meta, GOAL_Y_S, GOAL_Y_E); -} - auto puzzle::hash() const -> size_t { size_t h = 0; @@ -542,7 +390,7 @@ auto puzzle::try_get_invalid_reason() const -> std::optional const auto [w, h, gx, gy, r, g] = unpack_meta(); - traceln("Validating puzzle \"{}\"", string_repr()); + // traceln("Validating puzzle \"{}\"", string_repr()); const std::optional& b = try_get_target_block(); if (get_goal() && !b) { @@ -896,64 +744,8 @@ auto puzzle::try_move_block_at(const uint8_t x, const uint8_t y, const direction return p; } -auto puzzle::try_move_block_at_fast(const uint64_t bitmap, const uint8_t block_idx, - const direction dir) const -> std::optional -{ - const block b = block(repr.cooked.blocks[block_idx]); - const auto [bx, by, bw, bh, bt, bi] = b.unpack_repr(); - if (bi) { - return std::nullopt; - } - - const auto [w, h, gx, gy, r, g] = unpack_meta(); - const int dirs = r ? b.principal_dirs() : nor | eas | sou | wes; - - // Get target block - int _target_x = bx; - int _target_y = by; - switch (dir) { - case nor: - if (!(dirs & nor) || _target_y < 1) { - return std::nullopt; - } - --_target_y; - break; - case eas: - if (!(dirs & eas) || _target_x + bw >= w) { - return std::nullopt; - } - ++_target_x; - break; - case sou: - if (!(dirs & sou) || _target_y + bh >= h) { - return std::nullopt; - } - ++_target_y; - break; - case wes: - if (!(dirs & wes) || _target_x < 1) { - return std::nullopt; - } - --_target_x; - break; - } - - // Check collisions - const uint64_t bm = bitmap_clear_block(bitmap, b); - if (bitmap_check_collision(bm, b, dir)) { - return std::nullopt; - } - - // Replace block - const std::array blocks = sorted_replace(repr.cooked.blocks, block_idx, - block::create_repr( - _target_x, _target_y, bw, bh, bt)); - - // This constructor doesn't sort - return puzzle(std::make_tuple(w, h, gx, gy, r, g), blocks); -} - -auto puzzle::sorted_replace(std::array blocks, const uint8_t idx, +auto puzzle::sorted_replace(std::array blocks, + const uint8_t idx, const uint16_t new_val) -> std::array { // Remove old entry @@ -987,95 +779,69 @@ auto puzzle::blocks_bitmap() const -> uint64_t } auto [x, y, w, h, t, im] = b.unpack_repr(); + const uint8_t width = get_width(); for (int dy = 0; dy < h; ++dy) { for (int dx = 0; dx < w; ++dx) { - bitmap |= 1ULL << ((y + dy) * 8 + (x + dx)); + bitmap_set_bit(bitmap, width, x + dx, y + dy); } } } return bitmap; } -inline auto puzzle::bitmap_set_bit(const uint64_t bitmap, const uint8_t x, const uint8_t y) -> uint64_t +auto puzzle::blocks_bitmap_h() const -> uint64_t { - return bitmap & ~(1ULL << (y * 8 + x)); -} + uint64_t bitmap = 0; + for (uint8_t i = 0; i < MAX_BLOCKS; ++i) { + block b(repr.cooked.blocks[i]); + if (!b.valid()) { + break; + } + const int dirs = b.principal_dirs(); + if (!(dirs & eas)) { + continue; + } -inline auto puzzle::bitmap_get_bit(const uint64_t bitmap, const uint8_t x, const uint8_t y) -> bool -{ - return bitmap & (1ULL << (y * 8 + x)); -} + auto [x, y, w, h, t, im] = b.unpack_repr(); + const uint8_t width = get_width(); -auto puzzle::bitmap_clear_block(uint64_t bitmap, const block b) -> uint64_t -{ - const auto [x, y, w, h, t, i] = b.unpack_repr(); - - for (int dy = 0; dy < h; ++dy) { - for (int dx = 0; dx < w; ++dx) { - bitmap = bitmap_set_bit(bitmap, x + dx, y + dy); + for (int dy = 0; dy < h; ++dy) { + for (int dx = 0; dx < w; ++dx) { + bitmap_set_bit(bitmap, width, x + dx, y + dy); + } } } - return bitmap; } -auto puzzle::bitmap_check_collision(const uint64_t bitmap, const block b) -> bool +auto puzzle::blocks_bitmap_v() const -> uint64_t { - const auto [x, y, w, h, t, i] = b.unpack_repr(); + uint64_t bitmap = 0; + for (uint8_t i = 0; i < MAX_BLOCKS; ++i) { + block b(repr.cooked.blocks[i]); + if (!b.valid()) { + break; + } + const int dirs = b.principal_dirs(); + if (!(dirs & sou)) { + continue; + } - for (int dy = 0; dy < h; ++dy) { - for (int dx = 0; dx < w; ++dx) { - if (bitmap_get_bit(bitmap, x + dx, y + dy)) { - return true; // collision + auto [x, y, w, h, t, im] = b.unpack_repr(); + const uint8_t width = get_width(); + + for (int dy = 0; dy < h; ++dy) { + for (int dx = 0; dx < w; ++dx) { + bitmap_set_bit(bitmap, width, x + dx, y + dy); } } } - - return false; -} - -auto puzzle::bitmap_check_collision(const uint64_t bitmap, const block b, const direction dir) -> bool -{ - const auto [x, y, w, h, t, i] = b.unpack_repr(); - - switch (dir) { - case nor: // Check the row above: (x...x+w-1, y-1) - for (int dx = 0; dx < w; ++dx) { - if (bitmap_get_bit(bitmap, x + dx, y - 1)) { - return true; - } - } - break; - case sou: // Check the row below: (x...x+w-1, y+h) - for (int dx = 0; dx < w; ++dx) { - if (bitmap_get_bit(bitmap, x + dx, y + h)) { - return true; - } - } - break; - case wes: // Check the column left: (x-1, y...y+h-1) - for (int dy = 0; dy < h; ++dy) { - if (bitmap_get_bit(bitmap, x - 1, y + dy)) { - return true; - } - } - break; - case eas: // Check the column right: (x+w, y...y+h-1) - for (int dy = 0; dy < h; ++dy) { - if (bitmap_get_bit(bitmap, x + w, y + dy)) { - return true; - } - } - break; - } - return false; + return bitmap; } auto puzzle::explore_state_space() const -> std::pair, std::vector>> { - const std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); - std::vector state_pool; boost::unordered_flat_map state_indices; std::vector> links; @@ -1083,6 +849,11 @@ auto puzzle::explore_state_space() const -> std::pair, std:: // 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); @@ -1095,6 +866,24 @@ 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()); @@ -1106,10 +895,239 @@ auto puzzle::explore_state_space() const -> std::pair, std:: }); } - const std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); - - infoln("Explored puzzle. Took {} ms.", std::chrono::duration_cast(end - start).count()); - infoln("State space has size {} with {} transitions.", state_pool.size(), links.size()); - return {std::move(state_pool), std::move(links)}; +} + +auto puzzle::get_cluster_id_and_solution() const -> std::pair +{ + const auto& [puzzles, moves] = explore_state_space(); + bool solution = false; + 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; + } + } + return {min, solution}; +} + +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::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 +{ + if (!current_sequence.empty()) { + callback(current_sequence); + } + + if (current_sequence.size() == max_blocks) { + return; + } + + for (const block b : permitted_blocks) { + const int new_area = current_area + b.get_width() * b.get_height(); + if (new_area > board_area) { + continue; + } + + // 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); + } + } + + return; + } + + + 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); +} + +auto puzzle::explore_puzzle_space(const boost::unordered_flat_set& permitted_blocks, + const block target_block, + const std::tuple& target_block_pos_range, + const size_t max_blocks, + const std::optional* const> thread_pool) const -> + boost::unordered_flat_set +{ + const auto [w, h, gx, gy, r, g] = unpack_meta(); + + // Implemented in the slowest, stupidest way for now: + // 1. Iterate through all possible permitted_blocks permutations using recursive tree descent + // 2. Find the cluster id of the permutation by populating the entire state space + // - We could do some preprocessing to quickly reduce the numeric value + // of the state and check if its already contained in visited_clusters, + // this could save some state space calculations. + // 3. Add it to visited_clusters if unseen + + std::mutex mtx; + + boost::unordered_flat_set visited_clusters; + + // TODO: Can't even parallelize this. Or just start at different initial puzzles? + 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); + + return visited_clusters; } \ No newline at end of file diff --git a/src/renderer.cpp b/src/renderer.cpp index 47913fe..7613932 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -5,24 +5,20 @@ #include #include -#ifdef TRACY -#include -#endif - auto renderer::update_texture_sizes() -> void { if (!IsWindowResized()) { return; } - UnloadRenderTexture(render_target); + UnloadRenderTexture(graph_target); UnloadRenderTexture(klotski_target); UnloadRenderTexture(menu_target); const int width = GetScreenWidth() / 2; const int height = GetScreenHeight() - MENU_HEIGHT; - render_target = LoadRenderTexture(width, height); + graph_target = LoadRenderTexture(width, height); klotski_target = LoadRenderTexture(width, height); menu_target = LoadRenderTexture(width * 2, MENU_HEIGHT); } @@ -96,7 +92,7 @@ auto renderer::draw_mass_springs(const std::vector& masses) -> void rlUpdateVertexBuffer(color_vbo_id, colors.data(), colors.size() * sizeof(Color), 0); } - BeginTextureMode(render_target); + BeginTextureMode(graph_target); ClearBackground(RAYWHITE); BeginMode3D(camera.camera); @@ -195,7 +191,7 @@ auto renderer::draw_textures(const int fps, const int ups, const size_t mass_cou DrawTextureRec(klotski_target.texture, Rectangle(0, 0, klotski_target.texture.width, -klotski_target.texture.height), Vector2(0, MENU_HEIGHT), WHITE); - DrawTextureRec(render_target.texture, Rectangle(0, 0, render_target.texture.width, -render_target.texture.height), + DrawTextureRec(graph_target.texture, Rectangle(0, 0, graph_target.texture.width, -graph_target.texture.height), Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT), WHITE); // Draw borders diff --git a/src/state_manager.cpp b/src/state_manager.cpp index b1a1180..b6ad5a5 100644 --- a/src/state_manager.cpp +++ b/src/state_manager.cpp @@ -2,13 +2,6 @@ #include "graph_distances.hpp" #include "util.hpp" -#include -#include - -#ifdef TRACY -#include -#endif - auto state_manager::synced_try_insert_state(const puzzle& state) -> size_t { if (state_indices.contains(state)) { @@ -77,73 +70,22 @@ auto state_manager::synced_clear_statespace() -> void physics.clear_cmd(); } -auto state_manager::parse_preset_file(const std::string& _preset_file) -> bool +auto state_manager::save_current_to_preset_file(const std::string& preset_comment) -> void { - preset_file = _preset_file; - - std::ifstream file(preset_file); - if (!file) { - infoln("Preset file \"{}\" couldn't be loaded.", preset_file); - return false; + if (append_preset_file(preset_file, preset_comment, get_current_state())) { + current_preset = preset_states.size(); + reload_preset_file(); } - - std::string line; - std::vector comment_lines; - std::vector preset_lines; - while (std::getline(file, line)) { - if (line.starts_with("S")) { - preset_lines.push_back(line); - } else if (line.starts_with("#")) { - comment_lines.push_back(line); - } - } - - if (preset_lines.empty() || comment_lines.size() != preset_lines.size()) { - infoln("Preset file \"{}\" couldn't be loaded.", preset_file); - return false; - } - - preset_states.clear(); - for (const auto& preset : preset_lines) { - // Each char is a bit - const puzzle& p = puzzle(preset); - - if (const std::optional& reason = p.try_get_invalid_reason()) { - preset_states = {puzzle(4, 5, 0, 0, true, false)}; - infoln("Preset file \"{}\" contained invalid presets: {}", preset_file, *reason); - return false; - } - preset_states.emplace_back(p); - } - preset_comments = comment_lines; - - infoln("Loaded {} presets from \"{}\".", preset_lines.size(), preset_file); - - return true; } -auto state_manager::append_preset_file(const std::string& preset_name) -> bool +auto state_manager::reload_preset_file() -> void { - infoln(R"(Saving preset "{}" to "{}")", preset_name, preset_file); - - if (get_current_state().try_get_invalid_reason()) { - return false; + const auto [presets, comments] = parse_preset_file(preset_file); + if (!presets.empty()) { + preset_states = presets; + preset_comments = comments; } - - std::ofstream file(preset_file, std::ios_base::app | std::ios_base::out); - if (!file) { - infoln("Preset file \"{}\" couldn't be loaded.", preset_file); - return false; - } - - file << "\n# " << preset_name << "\n" << get_current_state().string_repr() << std::flush; - - infoln("Refreshing presets..."); - if (parse_preset_file(preset_file)) { - load_preset(preset_states.size() - 1); - } - - return true; + load_preset(current_preset); } auto state_manager::load_preset(const size_t preset) -> void @@ -298,14 +240,20 @@ auto state_manager::populate_graph() -> void const puzzle s = get_starting_state(); const puzzle p = get_current_state(); - // Clear the graph first so we don't add duplicates somehow synced_clear_statespace(); // Explore the entire statespace starting from the current state + const std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); + const auto& [states, _links] = s.explore_state_space(); synced_insert_statespace(states, _links); + const std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); + + infoln("Explored puzzle. Took {}ms.", std::chrono::duration_cast(end - start).count()); + infoln("State space has size {} with {} transitions.", state_pool.size(), links.size()); + current_state_index = state_indices[p]; previous_state_index = current_state_index; starting_state_index = state_indices[s]; diff --git a/src/threaded_physics.cpp b/src/threaded_physics.cpp index 859da31..e5414df 100644 --- a/src/threaded_physics.cpp +++ b/src/threaded_physics.cpp @@ -9,19 +9,15 @@ #include #include -#ifdef TRACY -#include -#endif - #ifdef ASYNC_OCTREE auto threaded_physics::set_octree_pool_thread_name(size_t idx) -> void { BS::this_thread::set_os_thread_name(std::format("octree-{}", idx)); - traceln("Using thread \"{}\"", BS::this_thread::get_os_thread_name().value_or("INVALID NAME")); + // traceln("Using thread \"{}\"", BS::this_thread::get_os_thread_name().value_or("INVALID NAME")); } #endif -auto threaded_physics::physics_thread(physics_state& state) -> void +auto threaded_physics::physics_thread(physics_state& state, const std::optional* const> thread_pool) -> void { mass_spring_system mass_springs; @@ -111,14 +107,14 @@ auto threaded_physics::physics_thread(physics_state& state) -> void #endif mass_springs.clear_forces(); - mass_springs.calculate_spring_forces(); - mass_springs.calculate_repulsion_forces(); - mass_springs.update(TIMESTEP * SIM_SPEED); + mass_springs.calculate_spring_forces(thread_pool); + mass_springs.calculate_repulsion_forces(thread_pool); + mass_springs.update(TIMESTEP * SIM_SPEED, thread_pool); // This is only helpful if we're drawing a grid at (0, 0, 0). Otherwise, it's just // expensive and yields no benefit since we can lock the camera to the center of mass // cheaply. - // mass_springs.center_masses(); + // mass_springs.center_masses(thread_pool); ++loop_iterations; physics_accumulator -= std::chrono::duration(TIMESTEP); diff --git a/src/user_interface.cpp b/src/user_interface.cpp index 82344ce..2ed907d 100644 --- a/src/user_interface.cpp +++ b/src/user_interface.cpp @@ -7,10 +7,6 @@ #define RAYGUI_IMPLEMENTATION #include -#ifdef TRACY -#include -#endif - auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height, const int _columns, const int _rows) -> void { @@ -639,13 +635,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_name.data(), 255, nullptr); + preset_comment.data(), 255, nullptr); if (button == 1) { - state.append_preset_file(preset_name.data()); + state.save_current_to_preset_file(preset_comment.data()); } if (button == 0 || button == 1 || button == 2) { save_window = false; - TextCopy(preset_name.data(), "\0"); + TextCopy(preset_comment.data(), "\0"); } }