From 47fcea6bcbd0cce5defb8182bafbbb6cd536e499 Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Wed, 18 Feb 2026 00:53:42 +0100 Subject: [PATCH] implement klotski graph closure solving + improve camera controls (panning) --- .clangd | 2 +- compile_commands.json | 2 +- include/config.hpp | 35 ++++++++++++------- include/klotski.hpp | 35 ++++++++++++------- include/mass_springs.hpp | 28 ++++++--------- include/renderer.hpp | 12 ++++--- src/klotski.cpp | 73 +++++++++++++++++++++++++++++++++++----- src/main.cpp | 59 +++++++++++++++++++++++--------- src/mass_springs.cpp | 35 ++++++++++++------- src/renderer.cpp | 41 +++++++++++++++++----- 10 files changed, 226 insertions(+), 96 deletions(-) diff --git a/.clangd b/.clangd index 602b81e..72fbebe 120000 --- a/.clangd +++ b/.clangd @@ -1 +1 @@ -./cmake-build-release/.clangd \ No newline at end of file +./cmake-build-debug/.clangd \ No newline at end of file diff --git a/compile_commands.json b/compile_commands.json index fd9db9d..66636ac 120000 --- a/compile_commands.json +++ b/compile_commands.json @@ -1 +1 @@ -./cmake-build-release/compile_commands.json \ No newline at end of file +./cmake-build-debug/compile_commands.json \ No newline at end of file diff --git a/include/config.hpp b/include/config.hpp index 1909a24..54d0d45 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -3,23 +3,32 @@ #include -constexpr int WIDTH = 800; -constexpr int HEIGHT = 800; +// Window +constexpr int WIDTH = 1000; +constexpr int HEIGHT = 1000; -constexpr float VERTEX_SIZE = 0.1; +// Camera Controls +constexpr float SIM_SPEED = 4.0; +constexpr float CAMERA_DISTANCE = 4.0; +constexpr float MIN_CAMERA_DISTANCE = 2.0; +constexpr float MAX_CAMERA_DISTANCE = 50.0; +constexpr float ZOOM_SPEED = 1.0; +constexpr float PAN_SPEED = 1.0; +constexpr float ROT_SPEED = 1.0; + +// Physics Engine +constexpr float SPRING_CONSTANT = 1.5; +constexpr float DAMPENING_CONSTANT = 0.8; +constexpr float REST_LENGTH = 1.0; +constexpr float REPULSION_FORCE = 0.05; +constexpr float VERLET_DAMPENING = 0.01; // [0, 1] + +// Graph Drawing +constexpr float VERTEX_SIZE = 0.05; constexpr Color VERTEX_COLOR = GREEN; constexpr Color EDGE_COLOR = DARKGREEN; -constexpr float SIM_SPEED = 4.0; -constexpr float ROTATION_SPEED = 1.0; -constexpr float CAMERA_DISTANCE = 2.2; -constexpr float CULLING_TOLERANCE = 0.1; // percentage - -constexpr float DEFAULT_SPRING_CONSTANT = 1.5; -constexpr float DEFAULT_DAMPENING_CONSTANT = 0.1; -constexpr float DEFAULT_REST_LENGTH = 0.5; -constexpr float DEFAULT_REPULSION_FORCE = 0.01; - +// Klotski Drawing constexpr int BOARD_PADDING = 5; constexpr int BLOCK_PADDING = 5; constexpr Color BLOCK_COLOR = DARKGREEN; diff --git a/include/klotski.hpp b/include/klotski.hpp index 7a7a808..bc9a551 100644 --- a/include/klotski.hpp +++ b/include/klotski.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include // #define DBG_PRINT @@ -109,17 +110,17 @@ public: ~Block() {} public: - auto Hash() -> int; + auto Hash() const -> int; - static auto Invalid() -> Block const; + static auto Invalid() -> Block; - auto IsValid() -> bool; + auto IsValid() const -> bool; - auto ToString() -> std::string; + auto ToString() const -> std::string; - auto Covers(int xx, int yy) -> bool; + auto Covers(int xx, int yy) const -> bool; - auto Collides(const Block &other) -> bool; + auto Collides(const Block &other) const -> bool; }; // A state is represented by a string "WxH:blocks", where W is the board width, @@ -130,8 +131,8 @@ public: // block's pivot being its top-left corner. class State { public: - int width; - int height; + const int width; + const int height; std::string state; // https://en.cppreference.com/w/cpp/iterator/input_iterator.html @@ -214,24 +215,32 @@ public: bool operator!=(const State &other) const { return !(*this == other); } - BlockIterator begin() { return BlockIterator(*this); } + BlockIterator begin() const { return BlockIterator(*this); } - BlockIterator end() { return BlockIterator(*this, width * height); } + BlockIterator end() const { return BlockIterator(*this, width * height); } ~State() {} public: - auto Hash() -> int; + auto Hash() const -> int; auto AddBlock(Block block) -> bool; - auto GetBlock(int x, int y) -> Block; + auto GetBlock(int x, int y) const -> Block; auto RemoveBlock(int x, int y) -> bool; auto MoveBlockAt(int x, int y, Direction dir) -> bool; - auto GetNextStates() -> std::vector; + auto GetNextStates() const -> std::vector; + + auto Closure() const + -> std::pair, + std::vector>>; +}; + +template <> struct std::hash { + std::size_t operator()(const State &s) const noexcept { return s.Hash(); } }; #endif diff --git a/include/mass_springs.hpp b/include/mass_springs.hpp index 306eb1d..81bcab8 100644 --- a/include/mass_springs.hpp +++ b/include/mass_springs.hpp @@ -1,21 +1,19 @@ #ifndef __MASS_SPRINGS_HPP_ #define __MASS_SPRINGS_HPP_ -#include #include #include #include #include -#include class Mass { public: - float mass; + const float mass; Vector3 position; Vector3 previous_position; // for verlet integration Vector3 velocity; Vector3 force; - bool fixed; + const bool fixed; public: Mass(float mass, Vector3 position, bool fixed) @@ -48,15 +46,13 @@ public: auto VerletUpdate(const float delta_time) -> void; }; -using MassList = std::vector; - class Spring { public: Mass &massA; Mass &massB; - float spring_constant; - float dampening_constant; - float rest_length; + const float spring_constant; + const float dampening_constant; + const float rest_length; public: Spring(Mass &massA, Mass &massB, float spring_constant, @@ -83,15 +79,13 @@ public: ~Spring() {} public: - auto CalculateSpringForce() -> void; + auto CalculateSpringForce() const -> void; }; -using SpringList = std::vector; - class MassSpringSystem { public: std::unordered_map masses; - SpringList springs; + std::unordered_map springs; public: MassSpringSystem() {}; @@ -104,8 +98,8 @@ public: ~MassSpringSystem() {}; public: - auto AddMass(float mass, Vector3 position, bool fixed, std::string state) - -> void; + auto AddMass(float mass, Vector3 position, bool fixed, + const std::string &state) -> void; auto GetMass(const std::string &state) -> Mass &; @@ -121,9 +115,9 @@ public: auto CalculateRepulsionForces() -> void; - auto EulerUpdate(const float delta_time) -> void; + auto EulerUpdate(float delta_time) -> void; - auto VerletUpdate(const float delta_time) -> void; + auto VerletUpdate(float delta_time) -> void; }; #endif diff --git a/include/renderer.hpp b/include/renderer.hpp index 6363f75..52e3cdb 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -20,11 +20,13 @@ private: float angle_y; Vector2 last_mouse; bool dragging; + bool panning; public: OrbitCamera3D(Vector3 target, float distance) : camera({0}), target(target), distance(distance), angle_x(0.0), - angle_y(0.3), last_mouse(Vector2Zero()), dragging(false) { + angle_y(0.3), last_mouse(Vector2Zero()), dragging(false), + panning(false) { camera.position = Vector3(0, 0, -1.0 * distance); camera.target = target; camera.up = Vector3(0, 1.0, 0); @@ -40,8 +42,8 @@ public: class Renderer { private: - int width; - int height; + const int width; + const int height; OrbitCamera3D camera; RenderTexture render_target; RenderTexture klotski_target; @@ -69,8 +71,8 @@ public: auto DrawMassSprings(const MassSpringSystem &masssprings) -> void; - auto DrawKlotski(State &state, int hov_x, int hov_y, int sel_x, int sel_y) - -> void; + auto DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x, + int sel_y) -> void; auto DrawTextures() -> void; }; diff --git a/src/klotski.cpp b/src/klotski.cpp index 1c00f15..2056083 100644 --- a/src/klotski.cpp +++ b/src/klotski.cpp @@ -1,20 +1,20 @@ #include "klotski.hpp" -auto Block::Hash() -> int { +auto Block::Hash() const -> int { std::string s = std::format("{},{},{},{}", x, y, width, height); return std::hash{}(s); } -auto Block::Invalid() -> Block const { +auto Block::Invalid() -> Block { Block block = Block(0, 0, 1, 1, false); block.width = 0; block.height = 0; return block; } -auto Block::IsValid() -> bool { return width != 0 && height != 0; } +auto Block::IsValid() const -> bool { return width != 0 && height != 0; } -auto Block::ToString() -> std::string { +auto Block::ToString() const -> std::string { if (target) { return std::format("{}{}", static_cast(width + static_cast('a') - 1), @@ -24,16 +24,16 @@ auto Block::ToString() -> std::string { } } -auto Block::Covers(int xx, int yy) -> bool { +auto Block::Covers(int xx, int yy) const -> bool { return xx >= x && xx < x + width && yy >= y && yy < y + height; } -auto Block::Collides(const Block &other) -> bool { +auto Block::Collides(const Block &other) const -> bool { return x < other.x + other.width && x + width > other.x && y < other.y + other.height && y + height > other.y; } -auto State::Hash() -> int { return std::hash{}(state); } +auto State::Hash() const -> int { return std::hash{}(state); } auto State::AddBlock(Block block) -> bool { if (block.x + block.width > width || block.y + block.height > height) { @@ -52,7 +52,7 @@ auto State::AddBlock(Block block) -> bool { return true; } -auto State::GetBlock(int x, int y) -> Block { +auto State::GetBlock(int x, int y) const -> Block { if (x >= width || y >= height) { return Block::Invalid(); } @@ -128,3 +128,60 @@ auto State::MoveBlockAt(int x, int y, Direction dir) -> bool { return true; } + +auto State::GetNextStates() const -> std::vector { + std::vector new_states; + + for (const Block &b : *this) { + State north = *this; + if (north.MoveBlockAt(b.x, b.y, Direction::NOR)) { + new_states.push_back(north); + } + + State east = *this; + if (east.MoveBlockAt(b.x, b.y, Direction::EAS)) { + new_states.push_back(east); + } + + State south = *this; + if (south.MoveBlockAt(b.x, b.y, Direction::SOU)) { + new_states.push_back(south); + } + + State west = *this; + if (west.MoveBlockAt(b.x, b.y, Direction::WES)) { + new_states.push_back(west); + } + } + + return new_states; +} + +auto State::Closure() const + -> std::pair, + std::vector>> { + std::unordered_set states; + std::vector> links; + + std::unordered_set remaining_states; + remaining_states.insert(*this); + + do { + const State current = *remaining_states.begin(); + remaining_states.erase(current); + + std::vector new_states = current.GetNextStates(); + for (State &s : new_states) { + if (!states.contains(s.state)) { + remaining_states.insert(s); + states.insert(s.state); + } + links.emplace_back(current.state, s.state); + } + } while (remaining_states.size() > 0); + + std::cout << "Closure contains " << states.size() << " states with " + << links.size() << " links." << std::endl; + + return std::make_pair(states, links); +} diff --git a/src/main.cpp b/src/main.cpp index 512a054..90c0a5a 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -15,23 +15,23 @@ auto klotski_a() -> State { Block b = Block(1, 0, 2, 2, true); Block c = Block(3, 0, 1, 2, false); Block d = Block(0, 2, 1, 2, false); - Block e = Block(1, 2, 2, 1, false); - Block f = Block(3, 2, 1, 2, false); - Block g = Block(1, 3, 1, 1, false); - Block h = Block(2, 3, 1, 1, false); - Block i = Block(0, 4, 1, 1, false); - Block j = Block(3, 4, 1, 1, false); + // Block e = Block(1, 2, 2, 1, false); + // Block f = Block(3, 2, 1, 2, false); + // Block g = Block(1, 3, 1, 1, false); + // Block h = Block(2, 3, 1, 1, false); + // Block i = Block(0, 4, 1, 1, false); + // Block j = Block(3, 4, 1, 1, false); s.AddBlock(a); s.AddBlock(b); s.AddBlock(c); s.AddBlock(d); - s.AddBlock(e); - s.AddBlock(f); - s.AddBlock(g); - s.AddBlock(h); - s.AddBlock(i); - s.AddBlock(j); + // s.AddBlock(e); + // s.AddBlock(f); + // s.AddBlock(g); + // s.AddBlock(h); + // s.AddBlock(i); + // s.AddBlock(j); return s; } @@ -58,6 +58,33 @@ auto main(int argc, char *argv[]) -> int { State board = klotski_a(); mass_springs.AddMass(1.0, Vector3Zero(), true, board.state); + // Closure solving + std::pair, + std::vector>> + closure = board.Closure(); + for (const auto &state : closure.first) { + Vector3 pos = + Vector3(static_cast(GetRandomValue(-10000, 10000)) / 1000.0, + static_cast(GetRandomValue(-10000, 10000)) / 1000.0, + static_cast(GetRandomValue(-10000, 10000)) / 1000.0); + + mass_springs.AddMass(1.0, pos, false, state); + } + for (const auto &[from, to] : closure.second) { + mass_springs.AddSpring(from, to, SPRING_CONSTANT, DAMPENING_CONSTANT, + REST_LENGTH); + } + std::cout << "Inserted " << mass_springs.masses.size() << " masses and " + << mass_springs.springs.size() << " springs." << std::endl; + std::cout << "Consuming " + << sizeof(decltype(*mass_springs.masses.begin())) * + mass_springs.masses.size() + << " Bytes for masses." << std::endl; + std::cout << "Consuming " + << sizeof(decltype(*mass_springs.springs.begin())) * + mass_springs.springs.size() + << " Bytes for springs." << std::endl; + // Rendering configuration Renderer renderer(WIDTH, HEIGHT); @@ -115,8 +142,9 @@ auto main(int argc, char *argv[]) -> int { if (board.MoveBlockAt(sel_x, sel_y, Direction::EAS)) { sel_x++; } + } else if (IsKeyPressed(KEY_P)) { + std::cout << board.state << std::endl; } - // TODO: Need to check for duplicate springs if (previous_state != board.state) { mass_springs.AddMass( 1.0, @@ -124,9 +152,8 @@ auto main(int argc, char *argv[]) -> int { static_cast(GetRandomValue(-1000, 1000)) / 1000.0, static_cast(GetRandomValue(-1000, 1000)) / 1000.0), false, board.state); - mass_springs.AddSpring(board.state, previous_state, - DEFAULT_SPRING_CONSTANT, - DEFAULT_DAMPENING_CONSTANT, DEFAULT_REST_LENGTH); + mass_springs.AddSpring(board.state, previous_state, SPRING_CONSTANT, + DAMPENING_CONSTANT, REST_LENGTH); } // Physics update diff --git a/src/mass_springs.cpp b/src/mass_springs.cpp index 469b6cd..a8b16b0 100644 --- a/src/mass_springs.cpp +++ b/src/mass_springs.cpp @@ -1,7 +1,7 @@ #include "mass_springs.hpp" #include "config.hpp" -#include +#include #include auto Mass::ClearForce() -> void { force = Vector3Zero(); } @@ -44,13 +44,13 @@ auto Mass::VerletUpdate(const float delta_time) -> void { Vector3 accel_term = Vector3Scale(acceleration, delta_time * delta_time); // Minimal dampening - displacement = Vector3Scale(displacement, 0.99); + displacement = Vector3Scale(displacement, 1.0 - VERLET_DAMPENING); position = Vector3Add(Vector3Add(position, displacement), accel_term); previous_position = temp_position; } -auto Spring::CalculateSpringForce() -> void { +auto Spring::CalculateSpringForce() const -> void { Vector3 delta_position; float current_length; Vector3 delta_velocity; @@ -74,7 +74,7 @@ auto Spring::CalculateSpringForce() -> void { } auto MassSpringSystem::AddMass(float mass, Vector3 position, bool fixed, - std::string state) -> void { + const std::string &state) -> void { if (!masses.contains(state)) { masses.insert(std::make_pair(state, Mass(mass, position, fixed))); } @@ -89,8 +89,18 @@ auto MassSpringSystem::AddSpring(const std::string &massA, float spring_constant, float dampening_constant, float rest_length) -> void { - springs.emplace_back(GetMass(massA), GetMass(massB), spring_constant, - dampening_constant, rest_length); + std::string states; + if (std::hash{}(massA) < std::hash{}(massB)) { + states = std::format("{}{}", massA, massB); + } else { + states = std::format("{}{}", massB, massA); + } + + if (!springs.contains(states)) { + springs.insert(std::make_pair( + states, Spring(GetMass(massA), GetMass(massB), spring_constant, + dampening_constant, rest_length))); + } } auto MassSpringSystem::Clear() -> void { @@ -105,7 +115,7 @@ auto MassSpringSystem::ClearForces() -> void { } auto MassSpringSystem::CalculateSpringForces() -> void { - for (auto &spring : springs) { + for (auto &[states, spring] : springs) { spring.CalculateSpringForce(); } } @@ -116,25 +126,24 @@ auto MassSpringSystem::CalculateRepulsionForces() -> void { Vector3 dx = Vector3Subtract(mass.position, m.position); // This can be accelerated with a spatial data structure - if (Vector3Length(dx) >= 3 * DEFAULT_REST_LENGTH) { + if (Vector3Length(dx) >= 3 * REST_LENGTH) { continue; } - mass.force = - Vector3Add(mass.force, Vector3Scale(Vector3Normalize(dx), - DEFAULT_REPULSION_FORCE)); + mass.force = Vector3Add( + mass.force, Vector3Scale(Vector3Normalize(dx), REPULSION_FORCE)); } } } -auto MassSpringSystem::EulerUpdate(const float delta_time) -> void { +auto MassSpringSystem::EulerUpdate(float delta_time) -> void { for (auto &[state, mass] : masses) { mass.CalculateVelocity(delta_time); mass.CalculatePosition(delta_time); } } -auto MassSpringSystem::VerletUpdate(const float delta_time) -> void { +auto MassSpringSystem::VerletUpdate(float delta_time) -> void { for (auto &[state, mass] : masses) { mass.VerletUpdate(delta_time); } diff --git a/src/renderer.cpp b/src/renderer.cpp index 224def5..da2f462 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -1,6 +1,7 @@ #include "renderer.hpp" #include +#include #include "config.hpp" #include "mass_springs.hpp" @@ -8,28 +9,50 @@ auto OrbitCamera3D::Update() -> void { Vector2 mouse = GetMousePosition(); - if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + if (IsMouseButtonPressed(MOUSE_RIGHT_BUTTON)) { dragging = true; last_mouse = mouse; + } else if (IsMouseButtonPressed(MOUSE_LEFT_BUTTON)) { + panning = true; + last_mouse = mouse; } - if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { + if (IsMouseButtonReleased(MOUSE_RIGHT_BUTTON)) { dragging = false; } + if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { + panning = false; + } if (dragging) { Vector2 dx = Vector2Subtract(mouse, last_mouse); last_mouse = mouse; - angle_x -= dx.x * 0.005; - angle_y += dx.y * 0.005; + angle_x -= dx.x * ROT_SPEED / 200.0; + angle_y += dx.y * ROT_SPEED / 200.0; angle_y = Clamp(angle_y, -1.5, 1.5); // Prevent flipping } + if (panning) { + Vector2 dx = Vector2Subtract(mouse, last_mouse); + last_mouse = mouse; + + float speed = distance * PAN_SPEED / 1000.0; + Vector3 forward = + Vector3Normalize(Vector3Subtract(camera.target, camera.position)); + Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up)); + Vector3 up = Vector3Normalize(Vector3CrossProduct(right, forward)); + + Vector3 offset = Vector3Add(Vector3Scale(right, -dx.x * speed), + Vector3Scale(up, dx.y * speed)); + + target = Vector3Add(target, offset); + } + float wheel = GetMouseWheelMove(); - distance -= wheel * 2.0; - distance = Clamp(distance, 2.0, 50.0); + distance -= wheel * ZOOM_SPEED; + distance = Clamp(distance, MIN_CAMERA_DISTANCE, MAX_CAMERA_DISTANCE); float x = cos(angle_y) * sin(angle_x) * distance; float y = sin(angle_y) * distance; @@ -48,7 +71,7 @@ auto Renderer::DrawMassSprings(const MassSpringSystem &masssprings) -> void { BeginMode3D(camera.camera); // Draw springs - for (const auto &spring : masssprings.springs) { + for (const auto &[states, spring] : masssprings.springs) { const Mass a = spring.massA; const Mass b = spring.massB; @@ -61,7 +84,7 @@ auto Renderer::DrawMassSprings(const MassSpringSystem &masssprings) -> void { VERTEX_COLOR); } - DrawGrid(10, 1.0); + // DrawGrid(10, 1.0); EndMode3D(); @@ -70,7 +93,7 @@ auto Renderer::DrawMassSprings(const MassSpringSystem &masssprings) -> void { EndTextureMode(); } -auto Renderer::DrawKlotski(State &state, int hov_x, int hov_y, int sel_x, +auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x, int sel_y) -> void { BeginTextureMode(klotski_target); ClearBackground(RAYWHITE);