add popups to certain user actions

This commit is contained in:
2026-02-28 21:29:57 +01:00
parent bc8dd423be
commit 809fbf1b93
20 changed files with 513 additions and 251 deletions

View File

@ -30,7 +30,7 @@ BraceWrapping:
SplitEmptyRecord: false SplitEmptyRecord: false
SplitEmptyNamespace: false SplitEmptyNamespace: false
BreakBeforeBraces: Custom BreakBeforeBraces: Custom
ColumnLimit: 120 ColumnLimit: 100
IncludeCategories: IncludeCategories:
- Regex: '^<.*' - Regex: '^<.*'
Priority: 1 Priority: 1

View File

@ -24,7 +24,8 @@ public:
auto pan(Vector2 last_mouse, Vector2 mouse) -> void; 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 #endif

View File

@ -5,9 +5,28 @@
#include "state_manager.hpp" #include "state_manager.hpp"
#include <functional> #include <functional>
#include <queue>
#include <raylib.h> #include <raylib.h>
#include <raymath.h> #include <raymath.h>
struct show_ok_message
{
std::string title;
std::string message;
};
struct show_yes_no_message
{
std::string title;
std::string message;
std::function<void(void)> on_yes;
};
struct show_save_preset_window
{};
using ui_command = std::variant<show_ok_message, show_yes_no_message, show_save_preset_window>;
class input_handler class input_handler
{ {
struct generic_handler struct generic_handler
@ -36,6 +55,8 @@ public:
state_manager& state; state_manager& state;
orbit_camera& camera; orbit_camera& camera;
std::queue<ui_command> ui_commands;
bool disable = false; bool disable = false;
// Block selection // Block selection
@ -115,7 +136,7 @@ public:
auto load_next_preset() -> void; auto load_next_preset() -> void;
auto goto_starting_state() -> void; auto goto_starting_state() -> void;
auto populate_graph() const -> void; auto populate_graph() const -> void;
auto clear_graph() const -> void; auto clear_graph() -> void;
auto toggle_mark_solutions() -> void; auto toggle_mark_solutions() -> void;
auto toggle_connect_solutions() -> void; auto toggle_connect_solutions() -> void;
auto toggle_mark_path() -> void; auto toggle_mark_path() -> void;
@ -132,18 +153,23 @@ public:
auto add_board_row() const -> void; auto add_board_row() const -> void;
auto toggle_editing() -> void; auto toggle_editing() -> void;
auto clear_goal() const -> void; auto clear_goal() const -> void;
auto save_preset() -> void;
// General // General
auto register_generic_handler(const std::function<void(input_handler&)>& handler) -> void; auto register_generic_handler(const std::function<void(input_handler&)>& handler) -> void;
auto register_mouse_pressed_handler(MouseButton button, const std::function<void(input_handler&)>& handler) -> void; auto register_mouse_pressed_handler(MouseButton button,
const std::function<void(input_handler&)>& handler) -> void;
auto register_mouse_released_handler(MouseButton button, const std::function<void(input_handler&)>& handler) auto register_mouse_released_handler(MouseButton button,
const std::function<void(input_handler&)>& handler)
-> void; -> void;
auto register_key_pressed_handler(KeyboardKey key, const std::function<void(input_handler&)>& handler) -> void; auto register_key_pressed_handler(KeyboardKey key,
const std::function<void(input_handler&)>& handler) -> void;
auto register_key_released_handler(KeyboardKey key, const std::function<void(input_handler&)>& handler) -> void; auto register_key_released_handler(KeyboardKey key,
const std::function<void(input_handler&)>& handler) -> void;
auto handle_input() -> void; auto handle_input() -> void;
}; };

View File

@ -41,7 +41,8 @@ public:
[[nodiscard]] auto get_octant(int node_idx, const Vector3& pos) const -> int; [[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<Vector3, Vector3>; [[nodiscard]] auto get_child_bounds(int node_idx, int octant) const
-> std::pair<Vector3, Vector3>;
auto insert(int node_idx, int mass_id, const Vector3& pos, float mass, int depth) -> void; auto insert(int node_idx, int mass_id, const Vector3& pos, float mass, int depth) -> void;

View File

@ -198,14 +198,8 @@ public:
auto clear_cmd() -> void; auto clear_cmd() -> void;
auto add_mass_springs_cmd(size_t num_masses, const std::vector<std::pair<size_t, size_t>>& springs) -> void; auto add_mass_springs_cmd(size_t num_masses,
}; const std::vector<std::pair<size_t, size_t>>& springs) -> void;
// https://en.cppreference.com/w/cpp/utility/variant/visit
template <class... Ts>
struct overloads : Ts...
{
using Ts::operator()...;
}; };
#endif #endif

View File

@ -48,8 +48,8 @@ public:
bool immovable = false; bool immovable = false;
public: public:
block(const int _x, const int _y, const int _width, const int _height, const bool _target = false, block(const int _x, const int _y, const int _width, const int _height,
const bool _immovable = false) const bool _target = false, const bool _immovable = false)
: x(_x), y(_y), width(_width), height(_height), target(_target), immovable(_immovable) : x(_x), y(_y), width(_width), height(_height), target(_target), immovable(_immovable)
{ {
if (_x < 0 || _x + _width > 9 || _y < 0 || _y + _height > 9) { if (_x < 0 || _x + _width > 9 || _y < 0 || _y + _height > 9) {
@ -84,7 +84,8 @@ public:
} }
immovable = false; immovable = false;
constexpr std::array<char, 9> immovable_chars{'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'}; constexpr std::array<char, 9> immovable_chars{'A', 'B', 'C', 'D', 'E',
'F', 'G', 'H', 'I'};
for (const char c : immovable_chars) { for (const char c : immovable_chars) {
if (b.contains(c)) { if (b.contains(c)) {
immovable = true; immovable = true;
@ -151,7 +152,8 @@ private:
explicit block_iterator(const puzzle& _state) : state(_state), current_pos(0) 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 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) 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), : 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) { if (w < 1 || w > 9 || h < 1 || h > 9) {
errln("State width/height must be in [1, 9]!"); errln("State width/height must be in [1, 9]!");
@ -217,8 +220,9 @@ public:
{} {}
explicit puzzle(const std::string& s) 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))), : width(std::stoi(s.substr(1, 1))), height(std::stoi(s.substr(2, 1))),
target_y(std::stoi(s.substr(4, 1))), restricted(s.substr(0, 1) == "R"), state(s) 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) { if (width < 1 || width > 9 || height < 1 || height > 9) {
errln("State width/height must be in [1, 9]!"); errln("State width/height must be in [1, 9]!");
@ -271,7 +275,7 @@ public:
[[nodiscard]] auto has_win_condition() const -> bool; [[nodiscard]] auto has_win_condition() const -> bool;
[[nodiscard]] auto won() const -> bool; [[nodiscard]] auto won() const -> bool;
[[nodiscard]] auto valid() const -> bool; [[nodiscard]] auto valid() const -> bool;
[[nodiscard]] auto valid_thorough() const -> bool; [[nodiscard]] auto try_get_invalid_reason() const -> std::optional<std::string>;
// Repr helpers // Repr helpers
@ -297,7 +301,8 @@ public:
// Playing // Playing
[[nodiscard]] auto try_move_block_at(int x, int y, direction dir) const -> std::optional<puzzle>; [[nodiscard]] auto try_move_block_at(int x, int y, direction dir) const
-> std::optional<puzzle>;
// Statespace // Statespace
@ -333,9 +338,11 @@ struct std::hash<std::pair<puzzle, puzzle>>
template <> template <>
struct std::equal_to<std::pair<puzzle, puzzle>> struct std::equal_to<std::pair<puzzle, puzzle>>
{ {
auto operator()(const std::pair<puzzle, puzzle>& a, const std::pair<puzzle, puzzle>& b) const noexcept -> bool auto operator()(const std::pair<puzzle, puzzle>& a,
const std::pair<puzzle, puzzle>& 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);
} }
}; };

View File

@ -18,8 +18,12 @@ private:
user_interface& gui; user_interface& gui;
const orbit_camera& camera; const orbit_camera& camera;
RenderTexture render_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); RenderTexture render_target =
RenderTexture klotski_target = LoadRenderTexture(GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT); 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); RenderTexture menu_target = LoadRenderTexture(GetScreenWidth(), MENU_HEIGHT);
// Batching // Batching
@ -31,7 +35,8 @@ private:
std::vector<Color> colors; std::vector<Color> colors;
Material vertex_mat = LoadMaterialDefault(); Material vertex_mat = LoadMaterialDefault();
Mesh cube_instance = GenMeshCube(VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE); 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; 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_MVP] = GetShaderLocation(instancing_shader, "mvp");
instancing_shader.locs[SHADER_LOC_MATRIX_MODEL] = instancing_shader.locs[SHADER_LOC_MATRIX_MODEL] =
GetShaderLocationAttrib(instancing_shader, "instanceTransform"); 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 vertexPosition: {}",
infoln("LOC instanceTransform: {}", rlGetLocationAttrib(instancing_shader.id, "instanceTransform")); rlGetLocationAttrib(instancing_shader.id, "vertexPosition"));
infoln("LOC instanceTransform: {}",
rlGetLocationAttrib(instancing_shader.id, "instanceTransform"));
infoln("LOC instanceColor: {}", rlGetLocationAttrib(instancing_shader.id, "instanceColor")); infoln("LOC instanceColor: {}", rlGetLocationAttrib(instancing_shader.id, "instanceColor"));
// vertex_mat.maps[MATERIAL_MAP_DIFFUSE].color = VERTEX_COLOR; // 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; auto draw_textures(int fps, int ups, size_t mass_count, size_t spring_count) const -> void;
public: public:
auto render(const std::vector<Vector3>& masses, int fps, int ups, size_t mass_count, size_t spring_count) -> void; auto render(const std::vector<Vector3>& masses, int fps, int ups, size_t mass_count,
size_t spring_count) -> void;
}; };
#endif #endif

View File

@ -22,16 +22,19 @@ private:
// State storage (store states twice for bidirectional lookup). // State storage (store states twice for bidirectional lookup).
// Everything else should only store indices to state_pool. // Everything else should only store indices to state_pool.
std::vector<puzzle> state_pool; // Indices are equal to mass_springs mass indices std::vector<puzzle> state_pool; // Indices are equal to mass_springs mass indices
std::unordered_map<puzzle, size_t> state_indices; // Maps states to indices std::unordered_map<puzzle, size_t> state_indices; // Maps states to indices
std::vector<std::pair<size_t, size_t>> links; // Indices are equal to mass_springs springs indices std::vector<std::pair<size_t, size_t>>
links; // Indices are equal to mass_springs springs indices
graph_distances node_target_distances; // Buffered and reused if the graph doesn't change graph_distances node_target_distances; // Buffered and reused if the graph doesn't change
std::unordered_set<size_t> winning_indices; // Indices of all states where the board is solved std::unordered_set<size_t> winning_indices; // Indices of all states where the board is solved
std::vector<size_t> winning_path; // Ordered list of node indices leading to the nearest solved state std::vector<size_t>
std::unordered_set<size_t> path_indices; // For faster lookup if a vertex is part of the path in renderer winning_path; // Ordered list of node indices leading to the nearest solved state
std::unordered_set<size_t>
path_indices; // For faster lookup if a vertex is part of the path in renderer
std::stack<size_t> move_history; // Moves between the starting state and the current state std::stack<size_t> move_history; // Moves between the starting state and the current state
std::unordered_map<size_t, int> visit_counts; // How often each state was visited std::unordered_map<size_t, int> visit_counts; // How often each state was visited
size_t starting_state_index = 0; size_t starting_state_index = 0;

View File

@ -22,13 +22,15 @@ class user_interface
const int padding; const int padding;
public: public:
grid(const int _x, const int _y, const int _width, const int _height, const int _columns, const int _rows, grid(const int _x, const int _y, const int _width, const int _height, const int _columns,
const int _padding) const int _rows, const int _padding)
: x(_x), y(_y), width(_width), height(_height), columns(_columns), rows(_rows), padding(_padding) : x(_x), y(_y), width(_width), height(_height), columns(_columns), rows(_rows),
padding(_padding)
{} {}
public: 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, int _width, int _height) -> void;
auto update_bounds(int _x, int _y) -> 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 bounds(int _x, int _y, int _width, int _height) const -> Rectangle;
[[nodiscard]] auto square_bounds() 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 struct style
@ -84,13 +87,22 @@ private:
grid menu_grid = grid(0, 0, GetScreenWidth(), MENU_HEIGHT, MENU_COLS, MENU_ROWS, MENU_PAD); 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, grid board_grid =
state.get_current_state().width, state.get_current_state().height, BOARD_PADDING); 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 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<void(void)> yes_no_handler;
bool ok_message = false;
bool yes_no_message = false;
bool save_window = false; bool save_window = false;
std::array<char, 256> preset_name = {}; std::array<char, 256> preset_name = {};
bool help_window = false; bool help_window = false;
@ -119,29 +131,34 @@ private:
static auto get_component_style(int component) -> component_style; static auto get_component_style(int component) -> component_style;
static auto set_component_style(int component, const component_style& style) -> void; 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, auto draw_button(Rectangle bounds, const std::string& label, Color color, bool enabled = true,
int font_size = FONT_SIZE) const -> int; 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, auto draw_menu_button(int x, int y, int width, int height, const std::string& label,
bool enabled = true, int font_size = FONT_SIZE) const -> int; 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, auto draw_toggle_slider(Rectangle bounds, const std::string& off_label,
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_menu_toggle_slider(int x, int y, int width, int height, const std::string& off_label, 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, const std::string& on_label, int* active, Color color,
int font_size = FONT_SIZE) const -> int; 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, auto draw_spinner(Rectangle bounds, const std::string& label, int* value, int min, int max,
bool enabled = true, int font_size = FONT_SIZE) const -> int; 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, auto draw_menu_spinner(int x, int y, int width, int height, const std::string& label,
Color color, bool enabled = true, int font_size = FONT_SIZE) const -> int; int* value, int min, int max, Color color, bool enabled = true,
int font_size = FONT_SIZE) const -> int;
auto draw_label(Rectangle bounds, const std::string& text, Color color, bool enabled = true, auto draw_label(Rectangle bounds, const std::string& text, Color color, bool enabled = true,
int font_size = FONT_SIZE) const -> int; 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; [[nodiscard]] auto window_open() const -> bool;
@ -158,10 +175,12 @@ public:
static auto get_background_color() -> Color; static auto get_background_color() -> Color;
auto help_popup() -> void; auto help_popup() -> void;
auto draw_save_preset_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_main_menu() -> void;
auto draw_puzzle_board() -> void; auto draw_puzzle_board() -> void;
auto draw_graph_overlay(int fps, int ups, size_t mass_count, size_t spring_count) -> 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 #endif

View File

@ -17,6 +17,13 @@ inline auto operator<<(std::ostream& os, const Vector3& v) -> std::ostream&
return os; return os;
} }
// https://en.cppreference.com/w/cpp/utility/variant/visit
template <class... Ts>
struct overloads : Ts...
{
using Ts::operator()...;
};
enum ctrl enum ctrl
{ {
reset = 0, reset = 0,

View File

@ -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 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up));
const Vector3 up = Vector3Normalize(Vector3CrossProduct(right, forward)); 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); target = Vector3Add(target, offset);
} }
auto orbit_camera::update(const Vector3& current_target, const Vector3& mass_center, const bool lock, auto orbit_camera::update(const Vector3& current_target, const Vector3& mass_center,
const bool mass_center_lock) -> void const bool lock, const bool mass_center_lock) -> void
{ {
if (lock) { if (lock) {
if (mass_center_lock) { if (mass_center_lock) {

View File

@ -18,7 +18,8 @@ auto graph_distances::empty() const -> bool
return distances.empty() || parents.empty() || nearest_targets.empty(); return distances.empty() || parents.empty() || nearest_targets.empty();
} }
auto graph_distances::calculate_distances(const size_t node_count, const std::vector<std::pair<size_t, size_t>>& edges, auto graph_distances::calculate_distances(const size_t node_count,
const std::vector<std::pair<size_t, size_t>>& edges,
const std::vector<size_t>& targets) -> void const std::vector<size_t>& targets) -> void
{ {
// Build a list of adjacent nodes to speed up BFS // Build a list of adjacent nodes to speed up BFS

View File

@ -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_LEFT, &input_handler::remove_board_column);
register_key_pressed_handler(KEY_X, &input_handler::clear_goal); 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_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_L, &input_handler::toggle_camera_lock);
register_key_pressed_handler(KEY_LEFT_ALT, &input_handler::toggle_camera_projection); 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; camera_panning = true;
// Enable this if the camera should be pannable even when locked (releasing the lock in the process): // Enable this if the camera should be pannable even when locked (releasing the lock in the
// camera_lock = false; // process): camera_lock = false;
} }
auto input_handler::camera_pan() const -> void auto input_handler::camera_pan() const -> void
@ -197,8 +198,8 @@ auto input_handler::add_block() -> void
block_add_y = -1; block_add_y = -1;
has_block_add_xy = false; has_block_add_xy = false;
} else if (current.covers(block_add_x, block_add_y, block_add_width, block_add_height)) { } else if (current.covers(block_add_x, block_add_y, block_add_width, block_add_height)) {
const std::optional<puzzle>& next = const std::optional<puzzle>& next = current.try_add_block(
current.try_add_block(puzzle::block(block_add_x, block_add_y, block_add_width, block_add_height, false)); puzzle::block(block_add_x, block_add_y, block_add_width, block_add_height, false));
if (next) { if (next) {
sel_x = block_add_x; sel_x = block_add_x;
@ -331,10 +332,19 @@ auto input_handler::load_previous_preset() -> void
return; return;
} }
block_add_x = -1; const auto handler = [&]()
block_add_y = -1; {
has_block_add_xy = false; block_add_x = -1;
state.load_previous_preset(); 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 auto input_handler::load_next_preset() -> void
@ -343,17 +353,31 @@ auto input_handler::load_next_preset() -> void
return; return;
} }
block_add_x = -1; const auto handler = [&]()
block_add_y = -1; {
has_block_add_xy = false; block_add_x = -1;
state.load_next_preset(); 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 auto input_handler::goto_starting_state() -> void
{ {
state.goto_starting_state(); const auto handler = [&]()
sel_x = 0; {
sel_y = 0; 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 auto input_handler::populate_graph() const -> void
@ -361,9 +385,14 @@ auto input_handler::populate_graph() const -> void
state.populate_graph(); 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 auto input_handler::toggle_mark_solutions() -> void
@ -508,6 +537,19 @@ auto input_handler::clear_goal() const -> void
state.edit_starting_state(*next); state.edit_starting_state(*next);
} }
auto input_handler::save_preset() -> void
{
if (!IsKeyDown(KEY_LEFT_CONTROL)) {
return;
}
if (const std::optional<std::string>& 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<void(input_handler&)>& handler) -> void auto input_handler::register_generic_handler(const std::function<void(input_handler&)>& handler) -> void
{ {
generic_handlers.push_back({handler}); generic_handlers.push_back({handler});

View File

@ -13,17 +13,10 @@
#include <tracy/Tracy.hpp> #include <tracy/Tracy.hpp>
#endif #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) // 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 // - Clear graph: Notify that this will clear the visited states and move
// history // history
// - Reset state: Notify that this will reset the move count // - Reset state: Notify that this will reset the move count
// - Next/Previous preset: Notify that this will clear all edits
// TODO: Reduce memory usage // TODO: Reduce memory usage
// - The memory model of the puzzle board is terrible (bitboards?) // - The memory model of the puzzle board is terrible (bitboards?)
@ -38,6 +31,11 @@
// given the initial state // given the initial state
// - Would allow to generate random puzzles with a certain move count // - 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. // NOTE: Tracy uses a huge amount of memory. For longer testing disable Tracy.
auto main(int argc, char* argv[]) -> int 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(); size_t current_index = state.get_current_index();
if (masses.size() > current_index) { if (masses.size() > current_index) {
const mass& current_mass = mass(masses.at(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 // Rendering

View File

@ -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 auto octree::get_octant(const int node_idx, const Vector3& pos) const -> int
{ {
const node& n = nodes[node_idx]; 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, auto [cx, cy, cz] =
(n.box_min.z + n.box_max.z) / 2.0f); 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 // 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. // 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; return octant;
} }
auto octree::get_child_bounds(const int node_idx, const int octant) const -> std::pair<Vector3, Vector3> auto octree::get_child_bounds(const int node_idx, const int octant) const
-> std::pair<Vector3, Vector3>
{ {
const node& n = nodes[node_idx]; 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, auto [cx, cy, cz] =
(n.box_min.z + n.box_max.z) / 2.0f); 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 min = Vector3Zero();
Vector3 max = 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); 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) auto octree::insert(const int node_idx, const int mass_id, const Vector3& pos, const float mass,
-> void const int depth) -> void
{ {
// infoln("Inserting position ({}, {}, {}) into octree at node {} (depth {})", pos.x, pos.y, pos.z, node_idx, // infoln("Inserting position ({}, {}, {}) into octree at node {} (depth {})", pos.x, pos.y,
// depth); // pos.z, node_idx, depth);
if (depth > MAX_DEPTH) { if (depth > MAX_DEPTH) {
errln("MAX_DEPTH! node={} box_min=({},{},{}) box_max=({},{},{}) pos=({},{},{})", node_idx, 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_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 // This runs from inside the physics thread so it won't exit cleanly
exit(1); 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 // We can place the particle in the empty leaf
if (nodes[node_idx].leaf && nodes[node_idx].mass_id == -1) { 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); insert(node_idx, mass_id, jittered, mass, depth);
return; return;
// Could also merge them, but that leads to the octree having less leafs than we have masses // Could also merge them, but that leads to the octree having less leafs than we have
// nodes[node_idx].mass_total += mass; // masses nodes[node_idx].mass_total += mass; return;
// return;
} }
// Convert the leaf to an internal node // 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 // Update the center of mass
const float new_mass = nodes[node_idx].mass_total + 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.x =
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.x * nodes[node_idx].mass_total + pos.x) / 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.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; nodes[node_idx].mass_total = new_mass;
} }

View File

@ -57,7 +57,8 @@ auto spring::calculate_spring_force(mass& _a, mass& _b) -> void
const Vector3 delta_velocity = Vector3Subtract(_a.velocity, _b.velocity); const Vector3 delta_velocity = Vector3Subtract(_a.velocity, _b.velocity);
const float hooke = SPRING_CONSTANT * (current_length - REST_LENGTH); 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_a = Vector3Scale(delta_position, -(hooke + dampening) * inv_current_length);
const Vector3 force_b = Vector3Scale(force_a, -1.0); const Vector3 force_b = Vector3Scale(force_a, -1.0);
@ -72,8 +73,8 @@ auto mass_spring_system::add_mass() -> void
// Done when adding springs // Done when adding springs
// Vector3 position{ // Vector3 position{
// static_cast<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100, 100)), // static_cast<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100,
// static_cast<float>(GetRandomValue(-100, 100)) // 100)), static_cast<float>(GetRandomValue(-100, 100))
// }; // };
// position = Vector3Scale(Vector3Normalize(position), REST_LENGTH * 2.0); // 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); const mass& mass_a = masses.at(a);
mass& mass_b = masses.at(b); mass& mass_b = masses.at(b);
Vector3 offset{static_cast<float>(GetRandomValue(-100, 100)), static_cast<float>(GetRandomValue(-100, 100)), Vector3 offset{static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100)),
static_cast<float>(GetRandomValue(-100, 100))}; static_cast<float>(GetRandomValue(-100, 100))};
offset = Vector3Normalize(offset) * REST_LENGTH; offset = Vector3Normalize(offset) * REST_LENGTH;
// If the offset moves the mass closer to the current center of mass, flip it // If the offset moves the mass closer to the current center of mass, flip it
if (!tree.nodes.empty()) { 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); const float mass_center_distance = Vector3Length(mass_center_direction);
if (mass_center_distance > 0 && Vector3DotProduct(offset, mass_center_direction) < 0.0f) { 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.position = mass_a.position + offset;
mass_b.previous_position = mass_b.position; 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); // mass_b.position.x, mass_b.position.y, mass_b.position.z);
springs.emplace_back(a, b); springs.emplace_back(a, b);
@ -201,7 +205,8 @@ auto mass_spring_system::calculate_repulsion_forces() -> void
// Calculate forces using Barnes-Hut // Calculate forces using Barnes-Hut
#ifdef THREADPOOL #ifdef THREADPOOL
const BS::multi_future<void> loop_future = threads.submit_loop(0, masses.size(), solve_octree, 256); const BS::multi_future<void> loop_future =
threads.submit_loop(0, masses.size(), solve_octree, 256);
loop_future.wait(); loop_future.wait();
#else #else
for (size_t i = 0; i < masses.size(); ++i) { 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.calculate_repulsion_forces();
mass_springs.verlet_update(TIMESTEP * SIM_SPEED); 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 // This is only helpful if we're drawing a grid at (0, 0, 0). Otherwise, it's just
// and yields no benefit since we can lock the camera to the center of mass cheaply. // expensive and yields no benefit since we can lock the camera to the center of mass
// mass_springs.center_masses(); // cheaply. mass_springs.center_masses();
++loop_iterations; ++loop_iterations;
physics_accumulator -= std::chrono::duration<double>(TIMESTEP); physics_accumulator -= std::chrono::duration<double>(TIMESTEP);
@ -309,7 +314,8 @@ auto threaded_physics::physics_thread(physics_state& state) -> void
#else #else
std::unique_lock<std::mutex> lock(state.data_mtx); std::unique_lock<std::mutex> lock(state.data_mtx);
#endif #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()) { if (!state.running.load()) {
// Running turned false while we were waiting for the condition // Running turned false while we were waiting for the condition
break; break;
@ -386,7 +392,8 @@ auto threaded_physics::clear_cmd() -> void
} }
auto threaded_physics::add_mass_springs_cmd(const size_t num_masses, auto threaded_physics::add_mass_springs_cmd(const size_t num_masses,
const std::vector<std::pair<size_t, size_t>>& springs) -> void const std::vector<std::pair<size_t, size_t>>& springs)
-> void
{ {
{ {
#ifdef TRACY #ifdef TRACY

View File

@ -1,5 +1,6 @@
#include "puzzle.hpp" #include "puzzle.hpp"
#include <optional>
#include <unordered_set> #include <unordered_set>
#ifdef TRACY #ifdef TRACY
@ -85,45 +86,64 @@ auto puzzle::valid() const -> bool
return width >= MIN_WIDTH && width <= MAX_WIDTH && height >= MIN_HEIGHT && height <= MAX_HEIGHT; 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<std::string>
{ {
if (has_win_condition() && !try_get_target_block()) { const std::optional<block>& b = try_get_target_block();
return false; 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); infoln("Validating puzzle {}", state);
if (static_cast<int>(state.length()) != width * height * 2 + PREFIX) { if (static_cast<int>(state.length()) != width * height * 2 + PREFIX) {
infoln("Puzzle invalid: Representation length {} doesn't match {}x{} board", state.length(), width, height); infoln("Puzzle invalid: Representation length {} doesn't match {}x{} board", state.length(),
return false; width, height);
return "Invalid Repr Length";
} }
// Check prefix // Check prefix
if (!std::string("FR").contains(state[0])) { if (!std::string("FR").contains(state[0])) {
infoln("Puzzle invalid: Representation[0] {} doesn't match [FR]", state[0]); infoln("Puzzle invalid: Representation[0] {} doesn't match [FR]", state[0]);
return false; return "Invalid Restricted Repr";
} }
if (restricted && state[0] != 'R') { if (restricted && state[0] != 'R') {
infoln("Puzzle invalid: Representation[0] {} doesn't match restricted={}", state[0], restricted); infoln("Puzzle invalid: Representation[0] {} doesn't match restricted={}", state[0],
return false; 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]); 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) { 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, infoln("Puzzle invalid: Representation[1/2] {}/{} doesn't match {}x{} board", state[1],
height); state[2], width, height);
return false; 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]); 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) { 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, infoln("Puzzle invalid: Representation[3/4] {}/{} doesn't match target ({}, {})", state[3],
target_y); state[4], target_x, target_y);
return false; return "Goal != Goal Repr";
} }
// Check blocks // Check blocks
@ -131,17 +151,19 @@ auto puzzle::valid_thorough() const -> bool
for (const char c : state.substr(PREFIX, state.length() - PREFIX)) { for (const char c : state.substr(PREFIX, state.length() - PREFIX)) {
if (!allowed_chars.contains(c)) { if (!allowed_chars.contains(c)) {
infoln("Puzzle invalid: Block {} has invalid character", 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) { if (!success) {
infoln("Puzzle invalid: Board size {}x{} not in range [3-9]", width, height); 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<block> auto puzzle::try_get_block(const int x, const int y) const -> std::optional<block>
@ -365,7 +387,8 @@ auto puzzle::try_toggle_wall(const int x, const int y) const -> std::optional<pu
return p; return p;
} }
auto puzzle::try_move_block_at(const int x, const int y, const direction dir) const -> std::optional<puzzle> auto puzzle::try_move_block_at(const int x, const int y, const direction dir) const
-> std::optional<puzzle>
{ {
const std::optional<block>& b = try_get_block(x, y); const std::optional<block>& b = try_get_block(x, y);
if (!b || b->immovable) { if (!b || b->immovable) {
@ -468,7 +491,8 @@ auto puzzle::find_adjacent_puzzles() const -> std::vector<puzzle>
return puzzles; return puzzles;
} }
auto puzzle::explore_state_space() const -> std::pair<std::vector<puzzle>, std::vector<std::pair<size_t, size_t>>> auto puzzle::explore_state_space() const
-> std::pair<std::vector<puzzle>, std::vector<std::pair<size_t, size_t>>>
{ {
#ifdef TRACY #ifdef TRACY
ZoneScoped; ZoneScoped;

View File

@ -73,10 +73,12 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
// Normal vertex // Normal vertex
Color c = VERTEX_COLOR; 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 // Winning vertex
c = VERTEX_TARGET_COLOR; 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 // Path vertex
c = VERTEX_PATH_COLOR; c = VERTEX_PATH_COLOR;
} else if (mass == state.get_starting_index()) { } else if (mass == state.get_starting_index()) {
@ -151,7 +153,8 @@ auto renderer::draw_mass_springs(const std::vector<Vector3>& masses) -> void
const size_t current_index = state.get_current_index(); const size_t current_index = state.get_current_index();
if (masses.size() > current_index) { if (masses.size() > current_index) {
const Vector3& current_mass = masses.at(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(); EndMode3D();
@ -186,36 +189,37 @@ auto renderer::draw_menu() const -> void
EndTextureMode(); EndTextureMode();
} }
auto renderer::draw_textures(const int fps, const int ups, const size_t mass_count, const size_t spring_count) const auto renderer::draw_textures(const int fps, const int ups, const size_t mass_count,
-> void const size_t spring_count) const -> void
{ {
BeginDrawing(); 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); Vector2(0, 0), WHITE);
DrawTextureRec(klotski_target.texture, DrawTextureRec(klotski_target.texture,
Rectangle(0, 0, klotski_target.texture.width, -klotski_target.texture.height), Rectangle(0, 0, klotski_target.texture.width, -klotski_target.texture.height),
Vector2(0, MENU_HEIGHT), WHITE); 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); Vector2(GetScreenWidth() / 2.0f, MENU_HEIGHT), WHITE);
// Draw borders // Draw borders
DrawRectangleLinesEx(Rectangle(0, 0, GetScreenWidth(), MENU_HEIGHT), 1.0f, BLACK); 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( 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); 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(fps, ups, mass_count, spring_count);
gui.draw_save_preset_popup();
gui.update();
EndDrawing(); EndDrawing();
} }
auto renderer::render(const std::vector<Vector3>& masses, const int fps, const int ups, const size_t mass_count, auto renderer::render(const std::vector<Vector3>& masses, const int fps, const int ups,
const size_t spring_count) -> void const size_t mass_count, const size_t spring_count) -> void
{ {
update_texture_sizes(); update_texture_sizes();

View File

@ -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<puzzle>& states, auto state_manager::synced_insert_statespace(const std::vector<puzzle>& states,
const std::vector<std::pair<size_t, size_t>>& _links) -> void const std::vector<std::pair<size_t, size_t>>& _links)
-> void
{ {
if (!state_pool.empty() || !state_indices.empty() || !links.empty()) { if (!state_pool.empty() || !state_indices.empty() || !links.empty()) {
warnln("Inserting statespace but collections haven't been cleared"); warnln("Inserting statespace but collections haven't been cleared");
@ -70,7 +71,7 @@ auto state_manager::synced_clear_statespace() -> void
winning_path.clear(); winning_path.clear();
path_indices.clear(); path_indices.clear();
// move_history does not get cleared here, but when resetting the board move_history = std::stack<size_t>();
visit_counts.clear(); visit_counts.clear();
// Queue an update to the physics engine state to keep in sync // 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) { for (const auto& preset : preset_lines) {
const puzzle& p = puzzle(preset); const puzzle& p = puzzle(preset);
if (!p.valid_thorough()) { if (const std::optional<std::string>& reason = p.try_get_invalid_reason()) {
preset_states = {puzzle(4, 5, 9, 9, false)}; 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; return false;
} }
preset_states.emplace_back(p); 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); 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; 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, // 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 the size grows, it was not a duplicate, and we can add the spring
if (state_pool.size() > size_before) { 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); 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 auto state_manager::clear_graph_and_add_current(const puzzle& p) -> void
{ {
// Do we need to make a copy before clearing the state_pool? // 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(); synced_clear_statespace();
// Re-add the current state // 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 // These states are no longer in the graph
previous_state_index = current_state_index; previous_state_index = current_state_index;

View File

@ -1,5 +1,6 @@
#include "user_interface.hpp" #include "user_interface.hpp"
#include "config.hpp" #include "config.hpp"
#include "input.hpp"
#include <raylib.h> #include <raylib.h>
@ -10,8 +11,9 @@
#include <tracy/Tracy.hpp> #include <tracy/Tracy.hpp>
#endif #endif
auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width, const int _height, auto user_interface::grid::update_bounds(const int _x, const int _y, const int _width,
const int _columns, const int _rows) -> void const int _height, const int _columns, const int _rows)
-> void
{ {
x = _x; x = _x;
y = _y; y = _y;
@ -21,7 +23,8 @@ auto user_interface::grid::update_bounds(const int _x, const int _y, const int _
rows = _rows; 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; x = _x;
y = _y; y = _y;
@ -45,7 +48,8 @@ auto user_interface::grid::bounds() const -> Rectangle
return bounds; 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) { if (_x < 0 || _x + _width > columns || _y < 0 || _y + _height > rows) {
errln("Grid bounds are outside range."); 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_width = (width - padding) / columns;
const int cell_height = (height - padding) / rows; const int cell_height = (height - padding) / rows;
return Rectangle(x + _x * cell_width + padding, y + _y * cell_height + padding, _width * cell_width - padding, return Rectangle(x + _x * cell_width + padding, y + _y * cell_height + padding,
_height * cell_height - padding); _width * cell_width - padding, _height * cell_height - padding);
} }
auto user_interface::grid::square_bounds() const -> Rectangle auto user_interface::grid::square_bounds() const -> Rectangle
@ -69,8 +73,8 @@ auto user_interface::grid::square_bounds() const -> Rectangle
return bounds; return bounds;
} }
auto user_interface::grid::square_bounds(const int _x, const int _y, const int _width, const int _height) const auto user_interface::grid::square_bounds(const int _x, const int _y, const int _width,
-> Rectangle const int _height) const -> Rectangle
{ {
// Assumes each cell is square, so either width or height are not completely // Assumes each cell is square, so either width or height are not completely
// filled // 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 x_offset = (width - grid_width) / 2;
const int y_offset = (height - grid_height) / 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, return Rectangle(x_offset + _x * (cell_size + padding) + padding,
_width * cell_size + padding * (_width - 1), _height * cell_size + padding * (_height - 1)); y_offset + _y * (cell_size + padding) + padding,
_width * cell_size + padding * (_width - 1),
_height * cell_size + padding * (_height - 1));
} }
auto user_interface::init() -> void 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 auto user_interface::get_component_style(const int component) -> component_style
{ {
return {{GuiGetStyle(component, BORDER_COLOR_NORMAL), GuiGetStyle(component, BASE_COLOR_NORMAL), return {
GuiGetStyle(component, TEXT_COLOR_NORMAL), GuiGetStyle(component, BORDER_COLOR_FOCUSED), {GuiGetStyle(component, BORDER_COLOR_NORMAL), GuiGetStyle(component, BASE_COLOR_NORMAL),
GuiGetStyle(component, BASE_COLOR_FOCUSED), GuiGetStyle(component, TEXT_COLOR_FOCUSED), GuiGetStyle(component, TEXT_COLOR_NORMAL), GuiGetStyle(component, BORDER_COLOR_FOCUSED),
GuiGetStyle(component, BORDER_COLOR_PRESSED), GuiGetStyle(component, BASE_COLOR_PRESSED), GuiGetStyle(component, BASE_COLOR_FOCUSED), GuiGetStyle(component, TEXT_COLOR_FOCUSED),
GuiGetStyle(component, TEXT_COLOR_PRESSED), GuiGetStyle(component, BORDER_COLOR_DISABLED), GuiGetStyle(component, BORDER_COLOR_PRESSED), GuiGetStyle(component, BASE_COLOR_PRESSED),
GuiGetStyle(component, BASE_COLOR_DISABLED), GuiGetStyle(component, TEXT_COLOR_DISABLED)}, GuiGetStyle(component, TEXT_COLOR_PRESSED), GuiGetStyle(component, BORDER_COLOR_DISABLED),
GuiGetStyle(component, BORDER_WIDTH), GuiGetStyle(component, BASE_COLOR_DISABLED), GuiGetStyle(component, TEXT_COLOR_DISABLED)},
GuiGetStyle(component, TEXT_PADDING), GuiGetStyle(component, BORDER_WIDTH),
GuiGetStyle(component, TEXT_ALIGNMENT)}; GuiGetStyle(component, TEXT_PADDING),
GuiGetStyle(component, TEXT_ALIGNMENT)};
} }
auto user_interface::set_component_style(const int component, const component_style& style) -> void 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); GuiSetStyle(component, TEXT_ALIGNMENT, style.text_alignment);
} }
auto user_interface::draw_button(const Rectangle bounds, const std::string& label, const Color color, auto user_interface::popup_bounds() -> Rectangle
const bool enabled, const int font_size) const -> int {
return Rectangle(static_cast<float>(GetScreenWidth()) / 2.0f - POPUP_WIDTH / 2.0f,
static_cast<float>(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 // Save original styling
const default_style original_default = get_default_style(); 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, 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 std::string& label, const Color color,
const int font_size) const -> int const bool enabled, const int font_size) const -> int
{ {
const Rectangle bounds = menu_grid.bounds(x, y, width, height); const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_button(bounds, label, color, enabled, font_size); return draw_button(bounds, label, color, enabled, font_size);
} }
auto user_interface::draw_toggle_slider(const Rectangle bounds, const std::string& off_label, 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, const std::string& on_label, int* active, Color color,
int font_size) const -> int bool enabled, int font_size) const -> int
{ {
// Save original styling // Save original styling
const default_style original_default = get_default_style(); 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; return pressed;
} }
auto user_interface::draw_menu_toggle_slider(const int x, const int y, const int width, const int height, auto user_interface::draw_menu_toggle_slider(const int x, const int y, const int width,
const std::string& off_label, const std::string& on_label, int* active, const int height, const std::string& off_label,
const Color color, const bool enabled, const int font_size) const -> int 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); const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_toggle_slider(bounds, off_label, on_label, active, color, enabled, font_size); 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, auto user_interface::draw_spinner(Rectangle bounds, const std::string& label, int* value, int min,
bool enabled, int font_size) const -> int int max, Color color, bool enabled, int font_size) const -> int
{ {
// Save original styling // Save original styling
const default_style original_default = get_default_style(); 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, 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 std::string& label, int* value, const int min,
const Color color, const bool enabled, const int font_size) const -> int 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); const Rectangle bounds = menu_grid.bounds(x, y, width, height);
return draw_spinner(bounds, label, value, min, max, color, enabled, font_size); return draw_spinner(bounds, label, value, min, max, color, enabled, font_size);
} }
auto user_interface::draw_label(const Rectangle bounds, const std::string& text, const Color color, const bool enabled, auto user_interface::draw_label(const Rectangle bounds, const std::string& text, const Color color,
const int font_size) const -> int const bool enabled, const int font_size) const -> int
{ {
// Save original styling // Save original styling
const default_style original_default = get_default_style(); 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; return pressed;
} }
auto user_interface::draw_board_block(const int x, const int y, const int width, const int height, const Color color, auto user_interface::draw_board_block(const int x, const int y, const int width, const int height,
const bool enabled) const -> bool const Color color, const bool enabled) const -> bool
{ {
component_style s = get_component_style(BUTTON); component_style s = get_component_style(BUTTON);
apply_block_color(s, color); 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 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 auto user_interface::draw_menu_header(const Color color) const -> void
{ {
int preset = state.get_current_preset(); int preset = static_cast<int>(state.get_current_preset());
draw_menu_spinner(0, 0, 1, 1, "Preset: ", &preset, -1, state.get_preset_count(), color, !input.editing); draw_menu_spinner(0, 0, 1, 1, "Preset: ", &preset, -1,
static_cast<int>(state.get_preset_count()), color, !input.editing);
if (preset > static_cast<int>(state.get_current_preset())) { if (preset > static_cast<int>(state.get_current_preset())) {
input.load_next_preset(); input.load_next_preset();
} else if (preset < static_cast<int>(state.get_current_preset())) { } else if (preset < static_cast<int>(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 auto user_interface::draw_graph_info(const Color color) const -> void
{ {
draw_menu_button(0, 1, 1, 1, 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); 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( draw_menu_button(2, 1, 1, 1,
2, 1, 1, 1, std::format("{} Moves to Nearest Solution",
std::format("{} Moves to Nearest Solution", state.get_path_length() > 0 ? state.get_path_length() - 1 : 0), state.get_path_length() > 0 ? state.get_path_length() - 1 : 0),
color); color);
} }
auto user_interface::draw_graph_controls(const Color color) const -> void 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; 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) { if (mark_solutions != input.mark_solutions) {
input.toggle_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 auto user_interface::draw_camera_controls(const Color color) const -> void
{ {
int lock_camera = input.camera_lock; 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) { if (lock_camera != input.camera_lock) {
input.toggle_camera_lock(); input.toggle_camera_lock();
} }
int lock_camera_mass_center = input.camera_mass_center_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, draw_menu_toggle_slider(1, 3, 1, 1, "Current Block (U)", "Graph Center (U)",
input.camera_lock); &lock_camera_mass_center, color, input.camera_lock);
if (lock_camera_mass_center != input.camera_mass_center_lock) { if (lock_camera_mass_center != input.camera_mass_center_lock) {
input.toggle_camera_mass_center_lock(); input.toggle_camera_mass_center_lock();
} }
int projection = camera.projection == CAMERA_ORTHOGRAPHIC; 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)) { if (projection != (camera.projection == CAMERA_ORTHOGRAPHIC)) {
input.toggle_camera_projection(); 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(); const int visits = state.get_current_visits();
draw_menu_button(0, 4, 1, 1, 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); color);
if (draw_menu_button(1, 4, 1, 1, "Make Optimal Move (Space)", color, state.has_distances())) { 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 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)) { if (draw_menu_button(2, 6, 1, 1, "Save as Preset", color)) {
save_window = true; if (const std::optional<std::string>& 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 // Returns the pressed button index
const int button = const int button = GuiTextInputBox(popup_bounds(), "Save as Preset", "Enter Preset Name",
GuiTextInputBox(Rectangle((GetScreenWidth() - POPUP_WIDTH) / 2.0f, (GetScreenHeight() - POPUP_HEIGHT) / 2.0f, "Ok;Cancel", preset_name.data(), 255, nullptr);
POPUP_WIDTH, POPUP_HEIGHT),
"Save as Preset", "Enter Preset Name", "Ok;Cancel", preset_name.data(), 255, nullptr);
if (button == 1) { if (button == 1) {
state.append_preset_file(preset_name.data()); 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 auto user_interface::draw_main_menu() -> void
{ {
menu_grid.update_bounds(0, 0, GetScreenWidth(), MENU_HEIGHT); 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(); const puzzle& current = state.get_current_state();
board_grid.update_bounds(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT, current.width, board_grid.update_bounds(0, MENU_HEIGHT, GetScreenWidth() / 2, GetScreenHeight() - MENU_HEIGHT,
current.height); current.width, current.height);
// Draw outer border // Draw outer border
const Rectangle bounds = board_grid.square_bounds(); const Rectangle bounds = board_grid.square_bounds();
DrawRectangleRec(bounds, current.won() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED); DrawRectangleRec(bounds, current.won() ? BOARD_COLOR_WON : BOARD_COLOR_RESTRICTED);
// Draw inner borders // Draw inner borders
DrawRectangle(bounds.x + BOARD_PADDING, bounds.y + BOARD_PADDING, bounds.width - 2 * BOARD_PADDING, DrawRectangle(bounds.x + BOARD_PADDING, bounds.y + BOARD_PADDING,
bounds.height - 2 * BOARD_PADDING, current.restricted ? BOARD_COLOR_RESTRICTED : BOARD_COLOR_FREE); bounds.width - 2 * BOARD_PADDING, bounds.height - 2 * BOARD_PADDING,
current.restricted ? BOARD_COLOR_RESTRICTED : BOARD_COLOR_FREE);
// Draw target opening // 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<puzzle::block> target_block = current.try_get_target_block(); const std::optional<puzzle::block> 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_x = current.target_x; const int target_y = current.target_y;
const int target_y = current.target_y; if (current.has_win_condition() && target_block) {
auto [x, y, width, height] = auto [x, y, width, height] = board_grid.square_bounds(
board_grid.square_bounds(target_x, target_y, target_block.value().width, target_block.value().height); 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) { if (target_x == 0) {
// Left opening // Left opening
@ -745,41 +812,82 @@ auto user_interface::draw_puzzle_board() -> void
// Draw block placing // Draw block placing
if (input.editing && input.has_block_add_xy) { 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 && if (current.covers(input.block_add_x, input.block_add_y) &&
input.hov_y >= input.block_add_y) { input.hov_x >= input.block_add_x && input.hov_y >= input.block_add_y) {
bool collides = false; bool collides = false;
for (const puzzle::block& b : current) { 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))) { input.hov_y - input.block_add_y + 1, false))) {
collides = true; collides = true;
break; break;
} }
} }
if (!collides) { 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); 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); graph_overlay_grid.update_bounds(GetScreenWidth() / 2, MENU_HEIGHT);
debug_overlay_grid.update_bounds(GetScreenWidth() / 2, GetScreenHeight() - 75); 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, 0, 1, 1),
draw_label(graph_overlay_grid.bounds(0, 1, 1, 1), std::format("FoV: {:0>6.2f}", camera.fov), BLACK); 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, 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); draw_label(graph_overlay_grid.bounds(0, 3, 1, 1), std::format("UPS: {:0>3}", ups), ORANGE);
// Debug // Debug
draw_label(debug_overlay_grid.bounds(0, 0, 1, 1), std::format("Debug:"), BLACK); draw_label(debug_overlay_grid.bounds(0, 0, 1, 1), std::format("Physics Debug:"), BLACK);
draw_label(debug_overlay_grid.bounds(0, 1, 1, 1), std::format("Masses: {}", mass_count), BLACK); draw_label(debug_overlay_grid.bounds(0, 1, 1, 1), std::format("Masses: {}", mass_count),
draw_label(debug_overlay_grid.bounds(0, 2, 1, 1), std::format("Springs: {}", spring_count), BLACK); 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(); 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();
} }