diff --git a/.clang-format b/.clang-format index 458293c..eec240f 100644 --- a/.clang-format +++ b/.clang-format @@ -12,7 +12,7 @@ AllowShortIfStatementsOnASingleLine: true AllowShortLoopsOnASingleLine: true AlwaysBreakAfterDefinitionReturnType: false AlwaysBreakTemplateDeclarations: Yes -BraceWrapping: +BraceWrapping: AfterCaseLabel: true AfterClass: true AfterControlStatement: false @@ -30,8 +30,8 @@ BraceWrapping: SplitEmptyRecord: false SplitEmptyNamespace: false BreakBeforeBraces: Custom -ColumnLimit: 120 -IncludeCategories: +ColumnLimit: 100 +IncludeCategories: - Regex: '^<.*' Priority: 1 - Regex: '^".*' diff --git a/include/camera.hpp b/include/camera.hpp index 6f59f1c..68efdfd 100644 --- a/include/camera.hpp +++ b/include/camera.hpp @@ -24,7 +24,8 @@ 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 diff --git a/include/input.hpp b/include/input.hpp index f7a0317..a70e655 100644 --- a/include/input.hpp +++ b/include/input.hpp @@ -5,9 +5,28 @@ #include "state_manager.hpp" #include +#include #include #include +struct show_ok_message +{ + std::string title; + std::string message; +}; + +struct show_yes_no_message +{ + std::string title; + std::string message; + std::function on_yes; +}; + +struct show_save_preset_window +{}; + +using ui_command = std::variant; + class input_handler { struct generic_handler @@ -36,6 +55,8 @@ public: state_manager& state; orbit_camera& camera; + std::queue ui_commands; + bool disable = false; // Block selection @@ -115,7 +136,7 @@ public: auto load_next_preset() -> void; auto goto_starting_state() -> void; auto populate_graph() const -> void; - auto clear_graph() const -> void; + auto clear_graph() -> void; auto toggle_mark_solutions() -> void; auto toggle_connect_solutions() -> void; auto toggle_mark_path() -> void; @@ -132,20 +153,25 @@ public: auto add_board_row() const -> void; auto toggle_editing() -> void; auto clear_goal() const -> void; + auto save_preset() -> void; // 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) + auto register_mouse_released_handler(MouseButton button, + 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; }; -#endif +#endif \ No newline at end of file diff --git a/include/octree.hpp b/include/octree.hpp index 1ffce4d..11573fc 100644 --- a/include/octree.hpp +++ b/include/octree.hpp @@ -41,7 +41,8 @@ public: [[nodiscard]] auto get_octant(int node_idx, const Vector3& pos) const -> int; - [[nodiscard]] auto get_child_bounds(int node_idx, int octant) const -> std::pair; + [[nodiscard]] auto get_child_bounds(int node_idx, int octant) const + -> std::pair; auto insert(int node_idx, int mass_id, const Vector3& pos, float mass, int depth) -> void; diff --git a/include/physics.hpp b/include/physics.hpp index 9faa1be..c2f8d26 100644 --- a/include/physics.hpp +++ b/include/physics.hpp @@ -198,14 +198,8 @@ public: auto clear_cmd() -> void; - auto add_mass_springs_cmd(size_t num_masses, const std::vector>& springs) -> void; -}; - -// https://en.cppreference.com/w/cpp/utility/variant/visit -template -struct overloads : Ts... -{ - using Ts::operator()...; + auto add_mass_springs_cmd(size_t num_masses, + const std::vector>& springs) -> void; }; #endif diff --git a/include/puzzle.hpp b/include/puzzle.hpp index 8fb9e29..2991e91 100644 --- a/include/puzzle.hpp +++ b/include/puzzle.hpp @@ -48,8 +48,8 @@ public: bool immovable = false; public: - block(const int _x, const int _y, const int _width, const int _height, const bool _target = false, - const bool _immovable = false) + block(const int _x, const int _y, const int _width, const int _height, + const bool _target = false, const bool _immovable = false) : x(_x), y(_y), width(_width), height(_height), target(_target), immovable(_immovable) { if (_x < 0 || _x + _width > 9 || _y < 0 || _y + _height > 9) { @@ -84,7 +84,8 @@ public: } immovable = false; - constexpr std::array immovable_chars{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'}; + constexpr std::array immovable_chars{'A', 'B', 'C', 'D', 'E', + 'F', 'G', 'H', 'I'}; for (const char c : immovable_chars) { if (b.contains(c)) { immovable = true; @@ -151,7 +152,8 @@ private: explicit block_iterator(const puzzle& _state) : state(_state), current_pos(0) {} - block_iterator(const puzzle& _state, const int _current_pos) : state(_state), current_pos(_current_pos) + block_iterator(const puzzle& _state, const int _current_pos) + : state(_state), current_pos(_current_pos) {} auto operator*() const -> block @@ -199,7 +201,8 @@ public: puzzle(const int w, const int h, const int tx, const int ty, const bool r) : width(w), height(h), target_x(tx), target_y(ty), restricted(r), - state(std::format("{}{}{}{}{}{}", r ? "R" : "F", w, h, tx, ty, std::string(w * h * 2, '.'))) + state( + std::format("{}{}{}{}{}{}", r ? "R" : "F", w, h, tx, ty, std::string(w * h * 2, '.'))) { if (w < 1 || w > 9 || h < 1 || h > 9) { errln("State width/height must be in [1, 9]!"); @@ -217,8 +220,9 @@ public: {} explicit puzzle(const std::string& s) - : width(std::stoi(s.substr(1, 1))), height(std::stoi(s.substr(2, 1))), target_x(std::stoi(s.substr(3, 1))), - target_y(std::stoi(s.substr(4, 1))), restricted(s.substr(0, 1) == "R"), state(s) + : width(std::stoi(s.substr(1, 1))), height(std::stoi(s.substr(2, 1))), + target_x(std::stoi(s.substr(3, 1))), target_y(std::stoi(s.substr(4, 1))), + restricted(s.substr(0, 1) == "R"), state(s) { if (width < 1 || width > 9 || height < 1 || height > 9) { errln("State width/height must be in [1, 9]!"); @@ -271,7 +275,7 @@ public: [[nodiscard]] auto has_win_condition() const -> bool; [[nodiscard]] auto won() const -> bool; [[nodiscard]] auto valid() const -> bool; - [[nodiscard]] auto valid_thorough() const -> bool; + [[nodiscard]] auto try_get_invalid_reason() const -> std::optional; // Repr helpers @@ -297,7 +301,8 @@ public: // Playing - [[nodiscard]] auto try_move_block_at(int x, int y, direction dir) const -> std::optional; + [[nodiscard]] auto try_move_block_at(int x, int y, direction dir) const + -> std::optional; // Statespace @@ -333,9 +338,11 @@ struct std::hash> template <> struct std::equal_to> { - auto operator()(const std::pair& a, const std::pair& b) const noexcept -> bool + auto operator()(const std::pair& a, + const std::pair& b) const noexcept -> bool { - return (a.first == b.first && a.second == b.second) || (a.first == b.second && a.second == b.first); + return (a.first == b.first && a.second == b.second) || + (a.first == b.second && a.second == b.first); } }; diff --git a/include/renderer.hpp b/include/renderer.hpp index 08c056f..4abe1e5 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -18,8 +18,12 @@ private: user_interface& gui; const orbit_camera& camera; - RenderTexture render_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); - RenderTexture klotski_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); + RenderTexture render_target = + LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); + + // TODO: Those should be moved to the user_interface.h + RenderTexture klotski_target = + LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); RenderTexture menu_target = LoadRenderTexture(GetScreenWidth(), MENU_HEIGHT); // Batching @@ -31,7 +35,8 @@ private: std::vector colors; Material vertex_mat = LoadMaterialDefault(); Mesh cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE); - Shader instancing_shader = LoadShader("shader/instancing_vertex.glsl", "shader/instancing_fragment.glsl"); + Shader instancing_shader = + LoadShader("shader/instancing_vertex.glsl", "shader/instancing_fragment.glsl"); unsigned int color_vbo_id = 0; @@ -43,10 +48,13 @@ public: instancing_shader.locs[SHADER_LOC_MATRIX_MVP] = GetShaderLocation(instancing_shader, "mvp"); instancing_shader.locs[SHADER_LOC_MATRIX_MODEL] = GetShaderLocationAttrib(instancing_shader, "instanceTransform"); - instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] = GetShaderLocation(instancing_shader, "viewPos"); + instancing_shader.locs[SHADER_LOC_VECTOR_VIEW] = + GetShaderLocation(instancing_shader, "viewPos"); - infoln("LOC vertexPosition: {}", rlGetLocationAttrib(instancing_shader.id, "vertexPosition")); - infoln("LOC instanceTransform: {}", rlGetLocationAttrib(instancing_shader.id, "instanceTransform")); + infoln("LOC vertexPosition: {}", + rlGetLocationAttrib(instancing_shader.id, "vertexPosition")); + infoln("LOC instanceTransform: {}", + rlGetLocationAttrib(instancing_shader.id, "instanceTransform")); infoln("LOC instanceColor: {}", rlGetLocationAttrib(instancing_shader.id, "instanceColor")); // vertex_mat.maps[MATERIAL_MAP_DIFFUSE].color = VERTEX_COLOR; @@ -94,7 +102,8 @@ private: auto draw_textures(int fps, int ups, size_t mass_count, size_t spring_count) const -> void; public: - auto render(const std::vector& masses, int fps, int ups, size_t mass_count, size_t spring_count) -> void; + auto render(const std::vector& masses, int fps, int ups, size_t mass_count, + size_t spring_count) -> void; }; #endif diff --git a/include/state_manager.hpp b/include/state_manager.hpp index 6f4db1c..75da808 100644 --- a/include/state_manager.hpp +++ b/include/state_manager.hpp @@ -22,16 +22,19 @@ 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 + std::vector state_pool; // Indices are equal to mass_springs mass indices std::unordered_map state_indices; // Maps states to indices - std::vector> links; // Indices are equal to mass_springs springs indices + std::vector> + links; // Indices are equal to mass_springs springs indices graph_distances node_target_distances; // Buffered and reused if the graph doesn't change std::unordered_set winning_indices; // Indices of all states where the board is solved - std::vector winning_path; // Ordered list of node indices leading to the nearest solved state - std::unordered_set path_indices; // For faster lookup if a vertex is part of the path in renderer + std::vector + winning_path; // Ordered list of node indices leading to the nearest solved state + std::unordered_set + path_indices; // For faster lookup if a vertex is part of the path in renderer - std::stack move_history; // Moves between the starting state and the current state + std::stack move_history; // Moves between the starting state and the current state std::unordered_map visit_counts; // How often each state was visited size_t starting_state_index = 0; diff --git a/include/user_interface.hpp b/include/user_interface.hpp index 07a77ed..66faba7 100644 --- a/include/user_interface.hpp +++ b/include/user_interface.hpp @@ -22,13 +22,15 @@ 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; @@ -36,7 +38,8 @@ 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 @@ -84,13 +87,22 @@ 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().width, state.get_current_state().height, BOARD_PADDING); + grid board_grid = + grid(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, + state.get_current_state().width, state.get_current_state().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 + + std::string message_title; + std::string message_message; + std::function yes_no_handler; + bool ok_message = false; + bool yes_no_message = false; bool save_window = false; std::array preset_name = {}; bool help_window = false; @@ -119,29 +131,34 @@ private: static auto get_component_style(int component) -> component_style; static auto set_component_style(int component, const component_style& style) -> void; + [[nodiscard]] static auto popup_bounds() -> Rectangle; + 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; + 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, 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, + int font_size = FONT_SIZE) const -> int; 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; @@ -158,10 +175,12 @@ public: static auto get_background_color() -> Color; auto help_popup() -> void; auto draw_save_preset_popup() -> void; + auto draw_ok_message_box() -> void; + auto draw_yes_no_message_box() -> void; auto draw_main_menu() -> void; auto draw_puzzle_board() -> void; auto draw_graph_overlay(int fps, int ups, size_t mass_count, size_t spring_count) -> void; - auto update() const -> void; + auto draw(int fps, int ups, size_t mass_count, size_t spring_count) -> void; }; #endif diff --git a/include/util.hpp b/include/util.hpp index 27e997a..3590319 100644 --- a/include/util.hpp +++ b/include/util.hpp @@ -17,6 +17,13 @@ inline auto operator<<(std::ostream& os, const Vector3& v) -> std::ostream& return os; } +// https://en.cppreference.com/w/cpp/utility/variant/visit +template +struct overloads : Ts... +{ + using Ts::operator()...; +}; + enum ctrl { reset = 0, diff --git a/src/camera.cpp b/src/camera.cpp index bcda4c7..721e935 100644 --- a/src/camera.cpp +++ b/src/camera.cpp @@ -35,13 +35,14 @@ 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) { diff --git a/src/distance.cpp b/src/distance.cpp index bbdf78a..e85b096 100644 --- a/src/distance.cpp +++ b/src/distance.cpp @@ -18,7 +18,8 @@ auto graph_distances::empty() const -> bool return distances.empty() || parents.empty() || nearest_targets.empty(); } -auto graph_distances::calculate_distances(const size_t node_count, const std::vector>& edges, +auto graph_distances::calculate_distances(const size_t node_count, + const std::vector>& edges, const std::vector& targets) -> void { // Build a list of adjacent nodes to speed up BFS diff --git a/src/input.cpp b/src/input.cpp index a92e553..f93ab00 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -53,6 +53,7 @@ auto input_handler::init_handlers() -> void register_key_pressed_handler(KEY_LEFT, &input_handler::remove_board_column); register_key_pressed_handler(KEY_X, &input_handler::clear_goal); register_key_pressed_handler(KEY_P, &input_handler::print_state); + register_key_pressed_handler(KEY_S, &input_handler::save_preset); // + CTRL register_key_pressed_handler(KEY_L, &input_handler::toggle_camera_lock); register_key_pressed_handler(KEY_LEFT_ALT, &input_handler::toggle_camera_projection); @@ -87,8 +88,8 @@ auto input_handler::camera_start_pan() -> void } camera_panning = true; - // Enable this if the camera should be pannable even when locked (releasing the lock in the process): - // camera_lock = false; + // Enable this if the camera should be pannable even when locked (releasing the lock in the + // process): camera_lock = false; } auto input_handler::camera_pan() const -> void @@ -197,8 +198,8 @@ auto input_handler::add_block() -> void block_add_y = -1; 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)); + const std::optional& next = current.try_add_block( + puzzle::block(block_add_x, block_add_y, block_add_width, block_add_height, false)); if (next) { sel_x = block_add_x; @@ -331,10 +332,19 @@ auto input_handler::load_previous_preset() -> void return; } - block_add_x = -1; - block_add_y = -1; - has_block_add_xy = false; - state.load_previous_preset(); + const auto handler = [&]() + { + block_add_x = -1; + block_add_y = -1; + has_block_add_xy = false; + state.load_previous_preset(); + }; + + if (state.was_edited()) { + ui_commands.emplace(show_yes_no_message{"Switch Preset?", "Edits Will Be Lost.", handler}); + } else { + handler(); + } } auto input_handler::load_next_preset() -> void @@ -343,17 +353,31 @@ auto input_handler::load_next_preset() -> void return; } - block_add_x = -1; - block_add_y = -1; - has_block_add_xy = false; - state.load_next_preset(); + const auto handler = [&]() + { + block_add_x = -1; + block_add_y = -1; + has_block_add_xy = false; + state.load_next_preset(); + }; + + if (state.was_edited()) { + ui_commands.emplace(show_yes_no_message{"Switch Preset?", "Edits Will Be Lost.", handler}); + } else { + handler(); + } } auto input_handler::goto_starting_state() -> void { - state.goto_starting_state(); - sel_x = 0; - sel_y = 0; + const auto handler = [&]() + { + state.goto_starting_state(); + sel_x = 0; + sel_y = 0; + }; + + ui_commands.emplace(show_yes_no_message{"Reset Board?", "This Clears the Move History.", handler}); } auto input_handler::populate_graph() const -> void @@ -361,9 +385,14 @@ auto input_handler::populate_graph() const -> void state.populate_graph(); } -auto input_handler::clear_graph() const -> void +auto input_handler::clear_graph() -> void { - state.clear_graph_and_add_current(); + const auto handler = [&]() + { + state.clear_graph_and_add_current(); + }; + + ui_commands.emplace(show_yes_no_message{"Clear Graph?", "This Clears the Move History.", handler}); } auto input_handler::toggle_mark_solutions() -> void @@ -508,6 +537,19 @@ auto input_handler::clear_goal() const -> void state.edit_starting_state(*next); } +auto input_handler::save_preset() -> void +{ + if (!IsKeyDown(KEY_LEFT_CONTROL)) { + return; + } + + if (const std::optional& reason = state.get_current_state().try_get_invalid_reason()) { + ui_commands.emplace(show_ok_message{"Can't Save Preset", std::format("Invalid Board: {}.", *reason)}); + } else { + ui_commands.emplace(show_save_preset_window{}); + } +} + auto input_handler::register_generic_handler(const std::function& handler) -> void { generic_handlers.push_back({handler}); @@ -570,4 +612,4 @@ auto input_handler::handle_input() -> void handler.handler(*this); } } -} +} \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 160a3e3..6023894 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,17 +13,10 @@ #include #endif -// TODO: Click states in the graph to display them in the board -// TODO: Move selection accordingly when undoing moves (need to diff two states -// and get the moved blocks) - // TODO: Add some popups (my split between input.cpp/gui.cpp makes this ugly) -// - Next move, goto target, goto worst: Notify that the graph needs to be -// populated // - Clear graph: Notify that this will clear the visited states and move // history // - Reset state: Notify that this will reset the move count -// - Next/Previous preset: Notify that this will clear all edits // TODO: Reduce memory usage // - The memory model of the puzzle board is terrible (bitboards?) @@ -38,6 +31,11 @@ // given the initial state // - Would allow to generate random puzzles with a certain move count +// 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 + // NOTE: Tracy uses a huge amount of memory. For longer testing disable Tracy. auto main(int argc, char* argv[]) -> int @@ -139,7 +137,8 @@ auto main(int argc, char* argv[]) -> int size_t current_index = state.get_current_index(); if (masses.size() > current_index) { const mass& current_mass = mass(masses.at(current_index)); - camera.update(current_mass.position, mass_center, input.camera_lock, input.camera_mass_center_lock); + camera.update(current_mass.position, mass_center, input.camera_lock, + input.camera_mass_center_lock); } // Rendering @@ -162,4 +161,4 @@ auto main(int argc, char* argv[]) -> int CloseWindow(); return 0; -} +} \ No newline at end of file diff --git a/src/octree.cpp b/src/octree.cpp index 3374696..e953bff 100644 --- a/src/octree.cpp +++ b/src/octree.cpp @@ -32,8 +32,9 @@ auto octree::create_empty_leaf(const Vector3& box_min, const Vector3& box_max) - auto octree::get_octant(const int node_idx, const Vector3& pos) const -> int { const node& n = nodes[node_idx]; - auto [cx, cy, cz] = Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f, - (n.box_min.z + n.box_max.z) / 2.0f); + auto [cx, cy, cz] = + Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f, + (n.box_min.z + n.box_max.z) / 2.0f); // The octant is encoded as a 3-bit integer "zyx". The node area is split // along all 3 axes, if a position is right of an axis, this bit is set to 1. @@ -53,11 +54,13 @@ auto octree::get_octant(const int node_idx, const Vector3& pos) const -> int return octant; } -auto octree::get_child_bounds(const int node_idx, const int octant) const -> std::pair +auto octree::get_child_bounds(const int node_idx, const int octant) const + -> std::pair { const node& n = nodes[node_idx]; - auto [cx, cy, cz] = Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f, - (n.box_min.z + n.box_max.z) / 2.0f); + auto [cx, cy, cz] = + Vector3((n.box_min.x + n.box_max.x) / 2.0f, (n.box_min.y + n.box_max.y) / 2.0f, + (n.box_min.z + n.box_max.z) / 2.0f); Vector3 min = Vector3Zero(); Vector3 max = Vector3Zero(); @@ -75,21 +78,23 @@ auto octree::get_child_bounds(const int node_idx, const int octant) const -> std return std::make_pair(min, max); } -auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, const float mass, const int depth) - -> void +auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, const float mass, + const int depth) -> void { - // infoln("Inserting position ({}, {}, {}) into octree at node {} (depth {})", pos.x, pos.y, pos.z, node_idx, - // depth); + // infoln("Inserting position ({}, {}, {}) into octree at node {} (depth {})", pos.x, pos.y, + // pos.z, node_idx, depth); if (depth > MAX_DEPTH) { errln("MAX_DEPTH! node={} box_min=({},{},{}) box_max=({},{},{}) pos=({},{},{})", node_idx, nodes[node_idx].box_min.x, nodes[node_idx].box_min.y, nodes[node_idx].box_min.z, - nodes[node_idx].box_max.x, nodes[node_idx].box_max.y, nodes[node_idx].box_max.z, pos.x, pos.y, pos.z); + nodes[node_idx].box_max.x, nodes[node_idx].box_max.y, nodes[node_idx].box_max.z, + pos.x, pos.y, pos.z); // This runs from inside the physics thread so it won't exit cleanly exit(1); } - // NOTE: Do not store a nodes[node_idx] reference as the nodes vector might reallocate during this function + // NOTE: Do not store a nodes[node_idx] reference as the nodes vector might reallocate during + // this function // We can place the particle in the empty leaf if (nodes[node_idx].leaf && nodes[node_idx].mass_id == -1) { @@ -116,9 +121,8 @@ auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, c insert(node_idx, mass_id, jittered, mass, depth); return; - // Could also merge them, but that leads to the octree having less leafs than we have masses - // nodes[node_idx].mass_total += mass; - // return; + // Could also merge them, but that leads to the octree having less leafs than we have + // masses nodes[node_idx].mass_total += mass; return; } // Convert the leaf to an internal node @@ -148,9 +152,12 @@ auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, c // Update the center of mass const float new_mass = nodes[node_idx].mass_total + mass; - nodes[node_idx].mass_center.x = (nodes[node_idx].mass_center.x * nodes[node_idx].mass_total + pos.x) / new_mass; - nodes[node_idx].mass_center.y = (nodes[node_idx].mass_center.y * nodes[node_idx].mass_total + pos.y) / new_mass; - nodes[node_idx].mass_center.z = (nodes[node_idx].mass_center.z * nodes[node_idx].mass_total + pos.z) / new_mass; + nodes[node_idx].mass_center.x = + (nodes[node_idx].mass_center.x * nodes[node_idx].mass_total + pos.x) / new_mass; + nodes[node_idx].mass_center.y = + (nodes[node_idx].mass_center.y * nodes[node_idx].mass_total + pos.y) / new_mass; + nodes[node_idx].mass_center.z = + (nodes[node_idx].mass_center.z * nodes[node_idx].mass_total + pos.z) / new_mass; nodes[node_idx].mass_total = new_mass; } diff --git a/src/physics.cpp b/src/physics.cpp index 7ee892b..b65a118 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -57,7 +57,8 @@ auto spring::calculate_spring_force(mass& _a, mass& _b) -> void const Vector3 delta_velocity = Vector3Subtract(_a.velocity, _b.velocity); const float hooke = SPRING_CONSTANT * (current_length - REST_LENGTH); - const float dampening = DAMPENING_CONSTANT * Vector3DotProduct(delta_velocity, delta_position) * inv_current_length; + const float dampening = + DAMPENING_CONSTANT * Vector3DotProduct(delta_velocity, delta_position) * inv_current_length; const Vector3 force_a = Vector3Scale(delta_position, -(hooke + dampening) * inv_current_length); const Vector3 force_b = Vector3Scale(force_a, -1.0); @@ -72,8 +73,8 @@ auto mass_spring_system::add_mass() -> void // Done when adding springs // Vector3 position{ - // static_cast(GetRandomValue(-100, 100)), static_cast(GetRandomValue(-100, 100)), - // static_cast(GetRandomValue(-100, 100)) + // static_cast(GetRandomValue(-100, 100)), static_cast(GetRandomValue(-100, + // 100)), static_cast(GetRandomValue(-100, 100)) // }; // position = Vector3Scale(Vector3Normalize(position), REST_LENGTH * 2.0); @@ -86,13 +87,15 @@ auto mass_spring_system::add_spring(size_t a, size_t b) -> void const mass& mass_a = masses.at(a); mass& mass_b = masses.at(b); - Vector3 offset{static_cast(GetRandomValue(-100, 100)), static_cast(GetRandomValue(-100, 100)), + Vector3 offset{static_cast(GetRandomValue(-100, 100)), + static_cast(GetRandomValue(-100, 100)), static_cast(GetRandomValue(-100, 100))}; offset = Vector3Normalize(offset) * REST_LENGTH; // If the offset moves the mass closer to the current center of mass, flip it if (!tree.nodes.empty()) { - const Vector3 mass_center_direction = Vector3Subtract(mass_a.position, tree.nodes.at(0).mass_center); + const Vector3 mass_center_direction = + Vector3Subtract(mass_a.position, tree.nodes.at(0).mass_center); const float mass_center_distance = Vector3Length(mass_center_direction); if (mass_center_distance > 0 && Vector3DotProduct(offset, mass_center_direction) < 0.0f) { @@ -103,7 +106,8 @@ auto mass_spring_system::add_spring(size_t a, size_t b) -> void mass_b.position = mass_a.position + offset; mass_b.previous_position = mass_b.position; - // infoln("Adding spring: ({}, {}, {})->({}, {}, {})", mass_a.position.x, mass_a.position.y, mass_a.position.z, + // infoln("Adding spring: ({}, {}, {})->({}, {}, {})", mass_a.position.x, mass_a.position.y, + // mass_a.position.z, // mass_b.position.x, mass_b.position.y, mass_b.position.z); springs.emplace_back(a, b); @@ -201,7 +205,8 @@ auto mass_spring_system::calculate_repulsion_forces() -> void // Calculate forces using Barnes-Hut #ifdef THREADPOOL - const BS::multi_future loop_future = threads.submit_loop(0, masses.size(), solve_octree, 256); + const BS::multi_future loop_future = + threads.submit_loop(0, masses.size(), solve_octree, 256); loop_future.wait(); #else for (size_t i = 0; i < masses.size(); ++i) { @@ -291,9 +296,9 @@ auto threaded_physics::physics_thread(physics_state& state) -> void mass_springs.calculate_repulsion_forces(); mass_springs.verlet_update(TIMESTEP * SIM_SPEED); - // 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(); + // 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(); ++loop_iterations; physics_accumulator -= std::chrono::duration(TIMESTEP); @@ -309,7 +314,8 @@ auto threaded_physics::physics_thread(physics_state& state) -> void #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; @@ -386,7 +392,8 @@ auto threaded_physics::clear_cmd() -> void } auto threaded_physics::add_mass_springs_cmd(const size_t num_masses, - const std::vector>& springs) -> void + const std::vector>& springs) + -> void { { #ifdef TRACY diff --git a/src/puzzle.cpp b/src/puzzle.cpp index 52dbf48..40f97be 100644 --- a/src/puzzle.cpp +++ b/src/puzzle.cpp @@ -1,5 +1,6 @@ #include "puzzle.hpp" +#include #include #ifdef TRACY @@ -85,45 +86,64 @@ auto puzzle::valid() const -> bool return width >= MIN_WIDTH && width <= MAX_WIDTH && height >= MIN_HEIGHT && height <= MAX_HEIGHT; } -auto puzzle::valid_thorough() const -> bool +auto puzzle::try_get_invalid_reason() const -> std::optional { - if (has_win_condition() && !try_get_target_block()) { - return false; + const std::optional& b = try_get_target_block(); + if (has_win_condition() && !b) { + return "Goal Without Target"; + } + if (!has_win_condition() && b) { + return "Target Without Goal"; + } + + if (has_win_condition() && b && restricted) { + const int dirs = b->principal_dirs(); + if ((dirs & nor && b->x != target_x) || (dirs & eas && b->y != target_y)) { + return "Goal Unreachable"; + } + } + + if (target_x > 0 && target_x + b->width < width && target_y > 0 && target_y + b->height < height) { + return "Goal Inside"; } infoln("Validating puzzle {}", state); if (static_cast(state.length()) != width * height * 2 + PREFIX) { - infoln("Puzzle invalid: Representation length {} doesn't match {}x{} board", state.length(), width, height); - return false; + infoln("Puzzle invalid: Representation length {} doesn't match {}x{} board", state.length(), + width, height); + return "Invalid Repr Length"; } // Check prefix if (!std::string("FR").contains(state[0])) { infoln("Puzzle invalid: Representation[0] {} doesn't match [FR]", state[0]); - return false; + return "Invalid Restricted Repr"; } if (restricted && state[0] != 'R') { - infoln("Puzzle invalid: Representation[0] {} doesn't match restricted={}", state[0], restricted); - return false; + infoln("Puzzle invalid: Representation[0] {} doesn't match restricted={}", state[0], + restricted); + return "Restricted != Restricted Repr"; } - if (!std::string("3456789").contains(state[1]) || !std::string("3456789").contains(state[2])) { + if (!std::string("0123456789").contains(state[1]) || + !std::string("0123456789").contains(state[2])) { infoln("Puzzle invalid: Representation[1/2] {}/{} doesn't match [3-9]", state[1], state[2]); - return false; + return "Invalid Dims Repr"; } if (std::stoi(state.substr(1, 1)) != width || std::stoi(state.substr(2, 1)) != height) { - infoln("Puzzle invalid: Representation[1/2] {}/{} doesn't match {}x{} board", state[1], state[2], width, - height); - return false; + infoln("Puzzle invalid: Representation[1/2] {}/{} doesn't match {}x{} board", state[1], + state[2], width, height); + return "Dims != Dims Repr"; } - if (!std::string("012345678").contains(state[3]) || !std::string("012345678").contains(state[4])) { + if (!std::string("0123456789").contains(state[3]) || + !std::string("0123456789").contains(state[4])) { infoln("Puzzle invalid: Representation[3/4] {}/{} doesn't match [1-9]", state[3], state[4]); - return false; + return "Invalid Goal Repr"; } if (std::stoi(state.substr(3, 1)) != target_x || std::stoi(state.substr(4, 1)) != target_y) { - infoln("Puzzle invalid: Representation[3/4] {}/{} doesn't match target ({}, {})", state[3], state[4], target_x, - target_y); - return false; + infoln("Puzzle invalid: Representation[3/4] {}/{} doesn't match target ({}, {})", state[3], + state[4], target_x, target_y); + return "Goal != Goal Repr"; } // Check blocks @@ -131,17 +151,19 @@ auto puzzle::valid_thorough() const -> bool for (const char c : state.substr(PREFIX, state.length() - PREFIX)) { if (!allowed_chars.contains(c)) { infoln("Puzzle invalid: Block {} has invalid character", c); - return false; + return "Invalid Block Repr"; } } - const bool success = width >= MIN_WIDTH && width <= MAX_WIDTH && height >= MIN_HEIGHT && height <= MAX_HEIGHT; + const bool success = + width >= MIN_WIDTH && width <= MAX_WIDTH && height >= MIN_HEIGHT && height <= MAX_HEIGHT; if (!success) { infoln("Puzzle invalid: Board size {}x{} not in range [3-9]", width, height); + return "Invalid Dims"; + } else { + return std::nullopt; } - - return success; } auto puzzle::try_get_block(const int x, const int y) const -> std::optional @@ -365,7 +387,8 @@ auto puzzle::try_toggle_wall(const int x, const int y) const -> std::optional std::optional +auto puzzle::try_move_block_at(const int x, const int y, const direction dir) const + -> std::optional { const std::optional& b = try_get_block(x, y); if (!b || b->immovable) { @@ -468,7 +491,8 @@ auto puzzle::find_adjacent_puzzles() const -> std::vector return puzzles; } -auto puzzle::explore_state_space() const -> std::pair, std::vector>> +auto puzzle::explore_state_space() const + -> std::pair, std::vector>> { #ifdef TRACY ZoneScoped; @@ -506,4 +530,4 @@ auto puzzle::explore_state_space() const -> std::pair, std:: infoln("State space has size {} with {} transitions.", state_pool.size(), links.size()); return std::make_pair(state_pool, links); -} +} \ No newline at end of file diff --git a/src/renderer.cpp b/src/renderer.cpp index 1b0f71a..70b92b8 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -73,10 +73,12 @@ auto renderer::draw_mass_springs(const std::vector& masses) -> void // Normal vertex Color c = VERTEX_COLOR; - if ((input.mark_solutions || input.mark_path) && state.get_winning_indices().contains(mass)) { + if ((input.mark_solutions || input.mark_path) && + state.get_winning_indices().contains(mass)) { // Winning vertex c = VERTEX_TARGET_COLOR; - } else if ((input.mark_solutions || input.mark_path) && state.get_path_indices().contains(mass)) { + } else if ((input.mark_solutions || input.mark_path) && + state.get_path_indices().contains(mass)) { // Path vertex c = VERTEX_PATH_COLOR; } else if (mass == state.get_starting_index()) { @@ -151,7 +153,8 @@ auto renderer::draw_mass_springs(const std::vector& masses) -> void const size_t current_index = state.get_current_index(); if (masses.size() > current_index) { const Vector3& current_mass = masses.at(current_index); - DrawCube(current_mass, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_CURRENT_COLOR); + DrawCube(current_mass, VERTEX_SIZE * 2, VERTEX_SIZE * 2, VERTEX_SIZE * 2, + VERTEX_CURRENT_COLOR); } EndMode3D(); @@ -186,36 +189,37 @@ auto renderer::draw_menu() const -> void EndTextureMode(); } -auto renderer::draw_textures(const int fps, const int ups, const size_t mass_count, const size_t spring_count) const - -> void +auto renderer::draw_textures(const int fps, const int ups, const size_t mass_count, + const size_t spring_count) const -> void { BeginDrawing(); - DrawTextureRec(menu_target.texture, Rectangle(0, 0, menu_target.texture.width, -menu_target.texture.height), + DrawTextureRec(menu_target.texture, + Rectangle(0, 0, menu_target.texture.width, -menu_target.texture.height), Vector2(0, 0), WHITE); 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(render_target.texture, + Rectangle(0, 0, render_target.texture.width, -render_target.texture.height), Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT), WHITE); // Draw borders DrawRectangleLinesEx(Rectangle(0, 0, GetScreenWidth(), MENU_HEIGHT), 1.0f, BLACK); - DrawRectangleLinesEx(Rectangle(0, MENU_HEIGHT, GetScreenWidth() / 2.0f, GetScreenHeight() - MENU_HEIGHT), 1.0f, - BLACK); DrawRectangleLinesEx( - Rectangle(GetScreenWidth() / 2.0f, MENU_HEIGHT, GetScreenWidth() / 2.0f, GetScreenHeight() - MENU_HEIGHT), 1.0f, + Rectangle(0, MENU_HEIGHT, GetScreenWidth() / 2.0f, GetScreenHeight() - MENU_HEIGHT), 1.0f, BLACK); + DrawRectangleLinesEx(Rectangle(GetScreenWidth() / 2.0f, MENU_HEIGHT, GetScreenWidth() / 2.0f, + GetScreenHeight() - MENU_HEIGHT), + 1.0f, BLACK); - gui.draw_graph_overlay(fps, ups, mass_count, spring_count); - gui.draw_save_preset_popup(); - gui.update(); + gui.draw(fps, ups, mass_count, spring_count); EndDrawing(); } -auto renderer::render(const std::vector& masses, const int fps, const int ups, const size_t mass_count, - const size_t spring_count) -> void +auto renderer::render(const std::vector& masses, const int fps, const int ups, + const size_t mass_count, const size_t spring_count) -> void { update_texture_sizes(); diff --git a/src/state_manager.cpp b/src/state_manager.cpp index d35e9db..298f6c7 100644 --- a/src/state_manager.cpp +++ b/src/state_manager.cpp @@ -35,7 +35,8 @@ 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"); @@ -70,7 +71,7 @@ auto state_manager::synced_clear_statespace() -> void winning_path.clear(); path_indices.clear(); - // move_history does not get cleared here, but when resetting the board + move_history = std::stack(); visit_counts.clear(); // Queue an update to the physics engine state to keep in sync @@ -107,9 +108,9 @@ auto state_manager::parse_preset_file(const std::string& _preset_file) -> bool for (const auto& preset : preset_lines) { const puzzle& p = puzzle(preset); - if (!p.valid_thorough()) { + if (const std::optional& reason = p.try_get_invalid_reason()) { preset_states = {puzzle(4, 5, 9, 9, false)}; - infoln("Preset file \"{}\" contained invalid presets.", preset_file); + infoln("Preset file \"{}\" contained invalid presets: {}", preset_file, *reason); return false; } preset_states.emplace_back(p); @@ -125,7 +126,7 @@ auto state_manager::append_preset_file(const std::string& preset_name) -> bool { infoln(R"(Saving preset "{}" to "{}")", preset_name, preset_file); - if (!get_current_state().valid_thorough()) { + if (get_current_state().try_get_invalid_reason()) { return false; } @@ -176,7 +177,8 @@ auto state_manager::update_current_state(const puzzle& p) -> void // Because synced_insert_link does not check for duplicates we do it here, // if the size grows, it was not a duplicate, and we can add the spring if (state_pool.size() > size_before) { - // The order is important, as the position of the second mass will be updated depending on the first + // The order is important, as the position of the second mass will be updated depending on + // the first synced_insert_link(current_state_index, index); } @@ -312,12 +314,12 @@ auto state_manager::populate_graph() -> void auto state_manager::clear_graph_and_add_current(const puzzle& p) -> void { // Do we need to make a copy before clearing the state_pool? - // const puzzle _p = p; + const puzzle _p = p; // NOLINT(*-unnecessary-copy-initialization) synced_clear_statespace(); // Re-add the current state - current_state_index = synced_try_insert_state(p); + current_state_index = synced_try_insert_state(_p); // These states are no longer in the graph previous_state_index = current_state_index; @@ -482,4 +484,4 @@ auto state_manager::get_total_moves() const -> size_t auto state_manager::was_edited() const -> bool { return preset_states.at(current_preset) != get_state(starting_state_index); -} +} \ No newline at end of file diff --git a/src/user_interface.cpp b/src/user_interface.cpp index 1809be9..6d9d52f 100644 --- a/src/user_interface.cpp +++ b/src/user_interface.cpp @@ -1,5 +1,6 @@ #include "user_interface.hpp" #include "config.hpp" +#include "input.hpp" #include @@ -10,8 +11,9 @@ #include #endif -auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height, - const int _columns, const int _rows) -> void +auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, + const int _height, const int _columns, const int _rows) + -> void { x = _x; y = _y; @@ -21,7 +23,8 @@ auto user_interface::grid::update_bounds(const int _x, const int _y, const int _ rows = _rows; } -auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height) -> void +auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, + const int _height) -> void { x = _x; y = _y; @@ -45,7 +48,8 @@ auto user_interface::grid::bounds() const -> Rectangle return bounds; } -auto user_interface::grid::bounds(const int _x, const int _y, const int _width, const int _height) const -> Rectangle +auto user_interface::grid::bounds(const int _x, const int _y, const int _width, + const int _height) const -> Rectangle { if (_x < 0 || _x + _width > columns || _y < 0 || _y + _height > rows) { errln("Grid bounds are outside range."); @@ -55,8 +59,8 @@ 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, - _height * cell_height - padding); + return Rectangle(x + _x * cell_width + padding, y + _y * cell_height + padding, + _width * cell_width - padding, _height * cell_height - padding); } auto user_interface::grid::square_bounds() const -> Rectangle @@ -69,8 +73,8 @@ 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, const int _height) const - -> Rectangle +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 // filled @@ -89,8 +93,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 @@ -194,15 +200,16 @@ auto user_interface::set_default_style(const default_style& style) -> void auto user_interface::get_component_style(const int component) -> component_style { - return {{GuiGetStyle(component, BORDER_COLOR_NORMAL), GuiGetStyle(component, BASE_COLOR_NORMAL), - GuiGetStyle(component, TEXT_COLOR_NORMAL), GuiGetStyle(component, BORDER_COLOR_FOCUSED), - GuiGetStyle(component, BASE_COLOR_FOCUSED), GuiGetStyle(component, TEXT_COLOR_FOCUSED), - GuiGetStyle(component, BORDER_COLOR_PRESSED), GuiGetStyle(component, BASE_COLOR_PRESSED), - GuiGetStyle(component, TEXT_COLOR_PRESSED), GuiGetStyle(component, BORDER_COLOR_DISABLED), - GuiGetStyle(component, BASE_COLOR_DISABLED), GuiGetStyle(component, TEXT_COLOR_DISABLED)}, - GuiGetStyle(component, BORDER_WIDTH), - GuiGetStyle(component, TEXT_PADDING), - GuiGetStyle(component, TEXT_ALIGNMENT)}; + return { + {GuiGetStyle(component, BORDER_COLOR_NORMAL), GuiGetStyle(component, BASE_COLOR_NORMAL), + GuiGetStyle(component, TEXT_COLOR_NORMAL), GuiGetStyle(component, BORDER_COLOR_FOCUSED), + GuiGetStyle(component, BASE_COLOR_FOCUSED), GuiGetStyle(component, TEXT_COLOR_FOCUSED), + GuiGetStyle(component, BORDER_COLOR_PRESSED), GuiGetStyle(component, BASE_COLOR_PRESSED), + GuiGetStyle(component, TEXT_COLOR_PRESSED), GuiGetStyle(component, BORDER_COLOR_DISABLED), + GuiGetStyle(component, BASE_COLOR_DISABLED), GuiGetStyle(component, TEXT_COLOR_DISABLED)}, + GuiGetStyle(component, BORDER_WIDTH), + GuiGetStyle(component, TEXT_PADDING), + GuiGetStyle(component, TEXT_ALIGNMENT)}; } auto user_interface::set_component_style(const int component, const component_style& style) -> void @@ -228,8 +235,16 @@ auto user_interface::set_component_style(const int component, const component_st GuiSetStyle(component, TEXT_ALIGNMENT, style.text_alignment); } -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::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); +} + +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(); @@ -260,16 +275,16 @@ auto user_interface::draw_button(const Rectangle bounds, const std::string& labe } auto user_interface::draw_menu_button(const int x, const int y, const int width, const int height, - const std::string& label, const Color color, const bool enabled, - const int font_size) const -> int + const std::string& label, const Color color, + const bool enabled, const int font_size) const -> int { const Rectangle bounds = menu_grid.bounds(x, y, width, height); return draw_button(bounds, label, color, enabled, font_size); } auto user_interface::draw_toggle_slider(const Rectangle bounds, const std::string& off_label, - const std::string& on_label, int* active, Color color, bool enabled, - int font_size) const -> int + const std::string& on_label, int* active, Color color, + bool enabled, int font_size) const -> int { // Save original styling const default_style original_default = get_default_style(); @@ -304,16 +319,18 @@ 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(); @@ -349,15 +366,16 @@ auto user_interface::draw_spinner(Rectangle bounds, const std::string& label, in } auto user_interface::draw_menu_spinner(const int x, const int y, const int width, const int height, - const std::string& label, int* value, const int min, const int max, - const Color color, const bool enabled, const int font_size) const -> int + const std::string& label, int* value, const int min, + const int max, const Color color, const bool enabled, + const int font_size) const -> int { const Rectangle bounds = menu_grid.bounds(x, y, width, height); return draw_spinner(bounds, label, value, min, max, color, enabled, font_size); } -auto user_interface::draw_label(const Rectangle bounds, const std::string& text, const Color color, const bool enabled, - const int font_size) const -> int +auto user_interface::draw_label(const Rectangle bounds, const std::string& text, const Color color, + const bool enabled, const int font_size) const -> int { // Save original styling const default_style original_default = get_default_style(); @@ -387,8 +405,8 @@ auto user_interface::draw_label(const Rectangle bounds, const std::string& text, return pressed; } -auto user_interface::draw_board_block(const int x, const int y, const int width, const int height, const Color color, - const bool enabled) const -> bool +auto user_interface::draw_board_block(const int x, const int y, const int width, const int height, + const Color color, const bool enabled) const -> bool { component_style s = get_component_style(BUTTON); apply_block_color(s, color); @@ -428,13 +446,14 @@ auto user_interface::draw_board_block(const int x, const int y, const int width, auto user_interface::window_open() const -> bool { - return save_window || help_window; + return save_window || help_window || ok_message || yes_no_message; } auto user_interface::draw_menu_header(const Color color) const -> void { - int preset = state.get_current_preset(); - draw_menu_spinner(0, 0, 1, 1, "Preset: ", &preset, -1, state.get_preset_count(), color, !input.editing); + int preset = static_cast(state.get_current_preset()); + draw_menu_spinner(0, 0, 1, 1, "Preset: ", &preset, -1, + static_cast(state.get_preset_count()), color, !input.editing); if (preset > static_cast(state.get_current_preset())) { input.load_next_preset(); } else if (preset < static_cast(state.get_current_preset())) { @@ -456,15 +475,17 @@ 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()), + std::format("Found {} States ({} Winning)", state.get_state_count(), + state.get_target_count()), color); - draw_menu_button(1, 1, 1, 1, std::format("Found {} Transitions", state.get_link_count()), color); + draw_menu_button(1, 1, 1, 1, std::format("Found {} Transitions", state.get_link_count()), + color); - draw_menu_button( - 2, 1, 1, 1, - std::format("{} Moves to Nearest Solution", state.get_path_length() > 0 ? state.get_path_length() - 1 : 0), - color); + draw_menu_button(2, 1, 1, 1, + std::format("{} Moves to Nearest Solution", + state.get_path_length() > 0 ? state.get_path_length() - 1 : 0), + color); } auto user_interface::draw_graph_controls(const Color color) const -> void @@ -485,7 +506,8 @@ auto user_interface::draw_graph_controls(const Color color) const -> void } int mark_solutions = input.mark_solutions; - draw_menu_toggle_slider(2, 2, 1, 1, "Solution Hidden (I)", "Solution Shown (I)", &mark_solutions, color); + draw_menu_toggle_slider(2, 2, 1, 1, "Solution Hidden (I)", "Solution Shown (I)", + &mark_solutions, color); if (mark_solutions != input.mark_solutions) { input.toggle_mark_solutions(); } @@ -495,20 +517,22 @@ auto user_interface::draw_graph_controls(const Color color) const -> void auto user_interface::draw_camera_controls(const Color color) const -> void { int lock_camera = input.camera_lock; - draw_menu_toggle_slider(0, 3, 1, 1, "Free Camera (L)", "Locked Camera (L)", &lock_camera, color); + draw_menu_toggle_slider(0, 3, 1, 1, "Free Camera (L)", "Locked Camera (L)", &lock_camera, + color); if (lock_camera != input.camera_lock) { input.toggle_camera_lock(); } int lock_camera_mass_center = input.camera_mass_center_lock; - draw_menu_toggle_slider(1, 3, 1, 1, "Current Block (U)", "Graph Center (U)", &lock_camera_mass_center, color, - input.camera_lock); + draw_menu_toggle_slider(1, 3, 1, 1, "Current Block (U)", "Graph Center (U)", + &lock_camera_mass_center, color, input.camera_lock); if (lock_camera_mass_center != input.camera_mass_center_lock) { input.toggle_camera_mass_center_lock(); } int projection = camera.projection == CAMERA_ORTHOGRAPHIC; - draw_menu_toggle_slider(2, 3, 1, 1, "Perspective (Alt)", "Orthographic (Alt)", &projection, color); + draw_menu_toggle_slider(2, 3, 1, 1, "Perspective (Alt)", "Orthographic (Alt)", &projection, + color); if (projection != (camera.projection == CAMERA_ORTHOGRAPHIC)) { input.toggle_camera_projection(); } @@ -535,7 +559,8 @@ auto user_interface::draw_puzzle_controls(const Color color) const -> void const int visits = state.get_current_visits(); draw_menu_button(0, 4, 1, 1, - std::format("{} Moves ({}{} Time at this State)", state.get_total_moves(), visits, nth(visits)), + 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())) { @@ -606,10 +631,18 @@ auto user_interface::draw_edit_controls(const Color color) const -> void auto user_interface::draw_menu_footer(const Color color) -> void { - draw_menu_button(0, 6, 2, 1, std::format("State: \"{}\"", state.get_current_state().state), color); + draw_menu_button(0, 6, 2, 1, std::format("State: \"{}\"", state.get_current_state().state), + color); if (draw_menu_button(2, 6, 1, 1, "Save as Preset", color)) { - save_window = true; + if (const std::optional& reason = + state.get_current_state().try_get_invalid_reason()) { + message_title = "Can't Save Preset"; + message_message = std::format("Invalid Board: {}.", *reason); + ok_message = true; + } else { + save_window = true; + } } } @@ -628,10 +661,8 @@ auto user_interface::draw_save_preset_popup() -> void } // Returns the pressed button index - const int button = - GuiTextInputBox(Rectangle((GetScreenWidth() - POPUP_WIDTH) / 2.0f, (GetScreenHeight() - POPUP_HEIGHT) / 2.0f, - POPUP_WIDTH, POPUP_HEIGHT), - "Save as Preset", "Enter Preset Name", "Ok;Cancel", preset_name.data(), 255, nullptr); + const int button = GuiTextInputBox(popup_bounds(), "Save as Preset", "Enter Preset Name", + "Ok;Cancel", preset_name.data(), 255, nullptr); if (button == 1) { state.append_preset_file(preset_name.data()); } @@ -641,6 +672,39 @@ auto user_interface::draw_save_preset_popup() -> void } } +auto user_interface::draw_ok_message_box() -> void +{ + if (!ok_message) { + return; + } + + const int button = + GuiMessageBox(popup_bounds(), message_title.data(), message_message.data(), "Ok"); + if (button == 0 || button == 1) { + message_title = ""; + message_message = ""; + ok_message = false; + } +} + +auto user_interface::draw_yes_no_message_box() -> void +{ + if (!yes_no_message) { + return; + } + + const int button = + GuiMessageBox(popup_bounds(), message_title.data(), message_message.data(), "Yes;No"); + if (button == 1) { + yes_no_handler(); + } + if (button == 0 || button == 1 || button == 2) { + message_title = ""; + message_message = ""; + yes_no_message = false; + } +} + auto user_interface::draw_main_menu() -> void { menu_grid.update_bounds(0, 0, GetScreenWidth(), MENU_HEIGHT); @@ -663,27 +727,30 @@ auto user_interface::draw_puzzle_board() -> void { const puzzle& current = state.get_current_state(); - board_grid.update_bounds(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, current.width, - current.height); + board_grid.update_bounds(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, + current.width, current.height); // Draw outer border const Rectangle bounds = board_grid.square_bounds(); DrawRectangleRec(bounds, current.won() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED); // Draw inner borders - DrawRectangle(bounds.x + BOARD_PADDING, bounds.y + BOARD_PADDING, bounds.width - 2 * BOARD_PADDING, - bounds.height - 2 * BOARD_PADDING, current.restricted ? BOARD_COLOR_RESTRICTED : BOARD_COLOR_FREE); + DrawRectangle(bounds.x + BOARD_PADDING, bounds.y + BOARD_PADDING, + bounds.width - 2 * BOARD_PADDING, bounds.height - 2 * BOARD_PADDING, + current.restricted ? BOARD_COLOR_RESTRICTED : BOARD_COLOR_FREE); // Draw target opening - // TODO: Only draw single direction (in corner) if restricted (use target block principal direction) + // TODO: Only draw single direction (in corner) if restricted (use target block principal + // direction) const std::optional target_block = current.try_get_target_block(); - if (current.has_win_condition() && target_block.has_value()) { - const int target_x = current.target_x; - const int target_y = current.target_y; - auto [x, y, width, height] = - board_grid.square_bounds(target_x, target_y, target_block.value().width, target_block.value().height); + const int target_x = current.target_x; + const int target_y = current.target_y; + if (current.has_win_condition() && target_block) { + auto [x, y, width, height] = board_grid.square_bounds( + target_x, target_y, target_block.value().width, target_block.value().height); - const Color opening_color = Fade(current.won() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, 0.3); + const Color opening_color = + Fade(current.won() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED, 0.3); if (target_x == 0) { // Left opening @@ -745,41 +812,82 @@ auto user_interface::draw_puzzle_board() -> void // Draw block placing if (input.editing && input.has_block_add_xy) { - if (current.covers(input.block_add_x, input.block_add_y) && input.hov_x >= input.block_add_x && - input.hov_y >= input.block_add_y) { + if (current.covers(input.block_add_x, input.block_add_y) && + input.hov_x >= input.block_add_x && input.hov_y >= input.block_add_y) { bool collides = false; for (const puzzle::block& b : current) { - if (b.collides(puzzle::block(input.block_add_x, input.block_add_y, input.hov_x - input.block_add_x + 1, + if (b.collides(puzzle::block(input.block_add_x, input.block_add_y, + input.hov_x - input.block_add_x + 1, input.hov_y - input.block_add_y + 1, false))) { collides = true; break; } } if (!collides) { - draw_board_block(input.block_add_x, input.block_add_y, input.hov_x - input.block_add_x + 1, + draw_board_block(input.block_add_x, input.block_add_y, + input.hov_x - input.block_add_x + 1, input.hov_y - input.block_add_y + 1, PURPLE); } } } + + // Draw goal boundaries when editing + if (input.editing) { + DrawRectangleLinesEx(board_grid.square_bounds(target_x, target_y, target_block->width, target_block->height), 2.0, TARGET_BLOCK_COLOR); + } } -auto user_interface::draw_graph_overlay(int fps, int ups, size_t mass_count, size_t spring_count) -> void +auto user_interface::draw_graph_overlay(int fps, int ups, size_t mass_count, size_t spring_count) + -> void { graph_overlay_grid.update_bounds(GetScreenWidth() / 2, MENU_HEIGHT); debug_overlay_grid.update_bounds(GetScreenWidth() / 2, GetScreenHeight() - 75); - draw_label(graph_overlay_grid.bounds(0, 0, 1, 1), std::format("Dist: {:0>7.2f}", camera.distance), BLACK); - draw_label(graph_overlay_grid.bounds(0, 1, 1, 1), std::format("FoV: {:0>6.2f}", camera.fov), BLACK); + draw_label(graph_overlay_grid.bounds(0, 0, 1, 1), + std::format("Dist: {:0>7.2f}", camera.distance), BLACK); + draw_label(graph_overlay_grid.bounds(0, 1, 1, 1), std::format("FoV: {:0>6.2f}", camera.fov), + BLACK); draw_label(graph_overlay_grid.bounds(0, 2, 1, 1), std::format("FPS: {:0>3}", fps), LIME); draw_label(graph_overlay_grid.bounds(0, 3, 1, 1), std::format("UPS: {:0>3}", ups), ORANGE); // Debug draw_label(debug_overlay_grid.bounds(0, 0, 1, 1), std::format("Debug:"), BLACK); - draw_label(debug_overlay_grid.bounds(0, 1, 1, 1), std::format("Masses: {}", mass_count), BLACK); - draw_label(debug_overlay_grid.bounds(0, 2, 1, 1), std::format("Springs: {}", spring_count), BLACK); + draw_label(debug_overlay_grid.bounds(0, 1, 1, 1), std::format("Masses: {}", mass_count), + BLACK); + draw_label(debug_overlay_grid.bounds(0, 2, 1, 1), std::format("Springs: {}", spring_count), + BLACK); } -auto user_interface::update() const -> void +auto user_interface::draw(const int fps, const int ups, const size_t mass_count, + const size_t spring_count) -> void { + const auto visitor = overloads{[&](const show_ok_message& msg) + { + message_title = msg.title; + message_message = msg.message; + ok_message = true; + }, + [&](const show_yes_no_message& msg) + { + message_title = msg.title; + message_message = msg.message; + yes_no_handler = msg.on_yes; + yes_no_message = true; + }, + [&](const show_save_preset_window& msg) { save_window = true; }}; + + while (!input.ui_commands.empty()) { + const ui_command& cmd = input.ui_commands.front(); + + cmd.visit(visitor); + + input.ui_commands.pop(); + } + input.disable = window_open(); -} + + draw_graph_overlay(fps, ups, mass_count, spring_count); + draw_save_preset_popup(); + draw_ok_message_box(); + draw_yes_no_message_box(); +} \ No newline at end of file