From 7faa8ecdb721f1796625aaf88cdd83d4f0aaf9cf Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Wed, 18 Feb 2026 03:16:13 +0100 Subject: [PATCH] allow to restrict block movement to principal block directions + add more samples --- include/config.hpp | 10 +-- include/klotski.hpp | 47 ++++++++--- src/klotski.cpp | 62 ++++++++++---- src/main.cpp | 199 ++++++++++++++++++++++++++++++++++++-------- src/renderer.cpp | 3 +- 5 files changed, 249 insertions(+), 72 deletions(-) diff --git a/include/config.hpp b/include/config.hpp index 8b352db..54f400e 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -9,11 +9,11 @@ constexpr int HEIGHT = 1300; // Camera Controls constexpr float SIM_SPEED = 4.0; -constexpr float CAMERA_DISTANCE = 4.0; +constexpr float CAMERA_DISTANCE = 250.0; constexpr float MIN_CAMERA_DISTANCE = 2.0; -constexpr float MAX_CAMERA_DISTANCE = 150.0; -constexpr float ZOOM_SPEED = 1.5; -constexpr float PAN_SPEED = 1.0; +constexpr float MAX_CAMERA_DISTANCE = 2000.0; +constexpr float ZOOM_SPEED = 10.0; +constexpr float PAN_SPEED = 2.0; constexpr float ROT_SPEED = 1.0; // Physics Engine @@ -21,7 +21,7 @@ constexpr float SPRING_CONSTANT = 1.5; constexpr float DAMPENING_CONSTANT = 0.8; constexpr float REST_LENGTH = 1.0; constexpr float REPULSION_FORCE = 0.1; -constexpr float REPULSION_RANGE = 3.0 * REST_LENGTH; +constexpr float REPULSION_RANGE = 5.0 * REST_LENGTH; constexpr float VERLET_DAMPENING = 0.01; // [0, 1] // Graph Drawing diff --git a/include/klotski.hpp b/include/klotski.hpp index bc9a551..85ecc77 100644 --- a/include/klotski.hpp +++ b/include/klotski.hpp @@ -118,6 +118,8 @@ public: auto ToString() const -> std::string; + auto GetPrincipalDirs() const -> int; + auto Covers(int xx, int yy) const -> bool; auto Collides(const Block &other) const -> bool; @@ -131,8 +133,9 @@ public: // block's pivot being its top-left corner. class State { public: - const int width; - const int height; + int width; + int height; + bool restricted; // Only allow blocks to move in their principal direction std::string state; // https://en.cppreference.com/w/cpp/iterator/input_iterator.html @@ -153,13 +156,13 @@ public: Block operator*() const { return Block(current_pos % state.width, current_pos / state.width, - state.state.substr(current_pos * 2 + 4, 2)); + state.state.substr(current_pos * 2 + 5, 2)); } BlockIterator &operator++() { do { current_pos++; - } while (state.state.substr(current_pos * 2 + 4, 2) == ".."); + } while (state.state.substr(current_pos * 2 + 5, 2) == ".."); return *this; } @@ -171,9 +174,9 @@ public: }; public: - State(int width, int height) - : width(width), height(height), - state(std::format("{}x{}:{}", width, height, + State(int width, int height, bool restricted) + : width(width), height(height), restricted(restricted), + state(std::format("{}{}x{}:{}", restricted ? "R" : "F", width, height, std::string(width * height * 2, '.'))) { if (width <= 0 || width >= 10 || height <= 0 || height >= 10) { std::cerr << "State width/height must be in [1, 9]!" << std::endl; @@ -187,8 +190,9 @@ public: } State(std::string state) - : width(std::stoi(state.substr(0, 1))), - height(std::stoi(state.substr(2, 1))), state(state) { + : width(std::stoi(state.substr(1, 1))), + height(std::stoi(state.substr(3, 1))), + restricted(state.substr(0, 1) == "R"), state(state) { if (width <= 0 || width >= 10 || height <= 0 || height >= 10) { std::cerr << "State width/height must be in [1, 9]!" << std::endl; exit(1); @@ -202,14 +206,31 @@ public: } State(const State ©) - : width(copy.width), height(copy.height), state(copy.state) {} + : width(copy.width), height(copy.height), restricted(copy.restricted), + state(copy.state) {} - State &operator=(const State ©) = delete; + State &operator=(const State ©) { + if (*this != copy) { + width = copy.width; + height = copy.height; + restricted = copy.restricted; + state = copy.state; + } + return *this; + } State(State &&move) - : width(move.width), height(move.height), state(std::move(move.state)) {} + : width(move.width), height(move.height), restricted(move.restricted), + state(std::move(move.state)) {} - State &operator=(State &&move) = delete; + State &operator=(State &&move) { + if (*this != move) { + width = move.width; + height = move.height; + restricted = move.restricted, state = std::move(move.state); + } + return *this; + }; bool operator==(const State &other) const { return state == other.state; } diff --git a/src/klotski.cpp b/src/klotski.cpp index 2056083..83f4150 100644 --- a/src/klotski.cpp +++ b/src/klotski.cpp @@ -24,6 +24,16 @@ auto Block::ToString() const -> std::string { } } +auto Block::GetPrincipalDirs() const -> int { + if (width > height) { + return Direction::EAS | Direction::WES; + } else if (height > width) { + return Direction::NOR | Direction::SOU; + } else { + return Direction::NOR | Direction::EAS | Direction::SOU | Direction::WES; + } +} + auto Block::Covers(int xx, int yy) const -> bool { return xx >= x && xx < x + width && yy >= y && yy < y + height; } @@ -46,7 +56,7 @@ auto State::AddBlock(Block block) -> bool { } } - int index = 4 + (width * block.y + block.x) * 2; + int index = 5 + (width * block.y + block.x) * 2; state.replace(index, 2, block.ToString()); return true; @@ -72,7 +82,7 @@ auto State::RemoveBlock(int x, int y) -> bool { return false; } - int index = 4 + (width * b.y + b.x) * 2; + int index = 5 + (width * b.y + b.x) * 2; state.replace(index, 2, ".."); return true; @@ -84,30 +94,34 @@ auto State::MoveBlockAt(int x, int y, Direction dir) -> bool { return false; } + int dirs = restricted ? block.GetPrincipalDirs() + : Direction::NOR | Direction::EAS | Direction::SOU | + Direction::WES; + // Get target block int target_x = block.x; int target_y = block.y; switch (dir) { case Direction::NOR: - if (target_y < 1) { + if (!(dirs & Direction::NOR) || target_y < 1) { return false; } target_y--; break; case Direction::EAS: - if (target_x + block.width >= width) { + if (!(dirs & Direction::EAS) || target_x + block.width >= width) { return false; } target_x++; break; case Direction::SOU: - if (target_y + block.height >= height) { + if (!(dirs & Direction::SOU) || target_y + block.height >= height) { return false; } target_y++; break; case Direction::WES: - if (target_x < 1) { + if (!(dirs & Direction::WES) || target_x < 1) { return false; } target_x--; @@ -133,24 +147,36 @@ 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); + int dirs = restricted ? b.GetPrincipalDirs() + : Direction::NOR | Direction::EAS | Direction::SOU | + Direction::WES; + + if (dirs & Direction::NOR) { + 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); + if (dirs & Direction::EAS) { + 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); + if (dirs & Direction::SOU) { + 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); + if (dirs & Direction::WES) { + State west = *this; + if (west.MoveBlockAt(b.x, b.y, Direction::WES)) { + new_states.push_back(west); + } } } diff --git a/src/main.cpp b/src/main.cpp index dcbf528..42283a6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,3 +1,4 @@ +#include #define VERLET_UPDATE #include @@ -12,8 +13,111 @@ #include "mass_springs.hpp" #include "renderer.hpp" -auto klotski_a() -> State { - State s = State(4, 5); +using StateGenerator = std::function; + +auto state_simple_1r() -> State { + State s = State(4, 5, true); + s.AddBlock(Block(0, 0, 1, 2, true)); + + return s; +} + +auto state_simple_1f() -> State { + State s = State(4, 5, false); + s.AddBlock(Block(0, 0, 1, 2, true)); + + return s; +} + +auto state_simple_2r() -> State { + State s = State(4, 5, true); + s.AddBlock(Block(0, 0, 1, 2, true)); + s.AddBlock(Block(1, 0, 1, 2, false)); + + return s; +} + +auto state_simple_2f() -> State { + State s = State(4, 5, false); + s.AddBlock(Block(0, 0, 1, 2, true)); + s.AddBlock(Block(1, 0, 1, 2, false)); + + return s; +} + +auto state_simple_3r() -> State { + State s = State(4, 5, true); + s.AddBlock(Block(0, 0, 1, 2, true)); + s.AddBlock(Block(1, 0, 1, 2, false)); + s.AddBlock(Block(2, 0, 1, 2, false)); + + return s; +} + +auto state_simple_3f() -> State { + State s = State(4, 5, false); + s.AddBlock(Block(0, 0, 1, 2, true)); + s.AddBlock(Block(1, 0, 1, 2, false)); + s.AddBlock(Block(2, 0, 1, 2, false)); + + return s; +} +auto state_complex_1r() -> State { + State s = State(6, 6, true); + s.AddBlock(Block(1, 0, 1, 3, false)); + s.AddBlock(Block(3, 0, 2, 1, false)); + s.AddBlock(Block(5, 0, 1, 3, false)); + s.AddBlock(Block(3, 2, 2, 1, true)); + s.AddBlock(Block(3, 3, 1, 2, false)); + s.AddBlock(Block(4, 4, 2, 1, false)); + + return s; +} + +auto state_complex_2r() -> State { + State s = State(6, 6, true); + s.AddBlock(Block(2, 0, 1, 3, false)); + s.AddBlock(Block(0, 2, 2, 1, true)); + s.AddBlock(Block(1, 3, 2, 1, false)); + s.AddBlock(Block(1, 4, 2, 1, false)); + s.AddBlock(Block(5, 4, 1, 2, false)); + s.AddBlock(Block(0, 5, 3, 1, false)); + + return s; +} + +auto state_complex_3r() -> State { + State s = State(6, 6, true); + s.AddBlock(Block(0, 0, 3, 1, false)); + s.AddBlock(Block(5, 0, 1, 3, false)); + s.AddBlock(Block(2, 2, 1, 2, false)); + s.AddBlock(Block(3, 2, 2, 1, true)); + s.AddBlock(Block(3, 3, 1, 2, false)); + s.AddBlock(Block(4, 3, 2, 1, false)); + s.AddBlock(Block(0, 4, 1, 2, false)); + s.AddBlock(Block(2, 4, 1, 2, false)); + s.AddBlock(Block(4, 4, 2, 1, false)); + s.AddBlock(Block(3, 5, 3, 1, false)); + + return s; +} + +auto state_complex_4f() -> State { + State s = State(4, 4, false); + s.AddBlock(Block(0, 0, 2, 1, false)); + s.AddBlock(Block(3, 0, 1, 1, false)); + s.AddBlock(Block(0, 1, 1, 2, false)); + s.AddBlock(Block(1, 1, 2, 2, true)); + s.AddBlock(Block(3, 1, 1, 1, false)); + s.AddBlock(Block(3, 2, 1, 1, false)); + s.AddBlock(Block(0, 3, 1, 1, false)); + s.AddBlock(Block(1, 3, 1, 1, false)); + + return s; +} + +auto state_klotski() -> State { + State s = State(4, 5, false); s.AddBlock(Block(0, 0, 1, 2, false)); s.AddBlock(Block(1, 0, 2, 2, true)); s.AddBlock(Block(3, 0, 1, 2, false)); @@ -28,34 +132,23 @@ auto klotski_a() -> State { return s; } -auto main(int argc, char *argv[]) -> int { - // if (argc < 2) { - // std::cout << "Missing .klotski file." << std::endl; - // return 1; - // } +std::array generators{ + state_simple_1r, state_simple_1f, state_simple_2r, state_simple_2f, + state_simple_3r, state_simple_3f, state_complex_1r, state_complex_2r, + state_complex_3r, state_complex_4f, state_klotski}; - std::cout << "OpenMP: " << omp_get_max_threads() << " threads." << std::endl; +auto apply_state(MassSpringSystem &mass_springs, StateGenerator generator) + -> State { + mass_springs.springs.clear(); + mass_springs.masses.clear(); - SetTraceLogLevel(LOG_ERROR); - - // SetTargetFPS(165); - SetConfigFlags(FLAG_VSYNC_HINT); - SetConfigFlags(FLAG_MSAA_4X_HINT); - // SetConfigFlags(FLAG_WINDOW_ALWAYS_RUN); - - InitWindow(WIDTH * 2, HEIGHT, "MassSprings"); - - // Mass springs configuration - MassSpringSystem mass_springs; - - // Klotski configuration - State board = klotski_a(); - mass_springs.AddMass(1.0, Vector3Zero(), true, board.state); + State s = generator(); + mass_springs.AddMass(1.0, Vector3Zero(), false, s.state); // Closure solving std::pair, std::vector>> - closure = board.Closure(); + closure = s.Closure(); for (const auto &state : closure.first) { Vector3 pos = Vector3(static_cast(GetRandomValue(-10000, 10000)) / 1000.0, @@ -79,9 +172,34 @@ auto main(int argc, char *argv[]) -> int { mass_springs.springs.size() << " Bytes for springs." << std::endl; + return s; +}; + +auto main(int argc, char *argv[]) -> int { + // if (argc < 2) { + // std::cout << "Missing .klotski file." << std::endl; + // return 1; + // } + + std::cout << "OpenMP: " << omp_get_max_threads() << " threads." << std::endl; + + SetTraceLogLevel(LOG_ERROR); + + // SetTargetFPS(60); + SetConfigFlags(FLAG_VSYNC_HINT); + SetConfigFlags(FLAG_MSAA_4X_HINT); + // SetConfigFlags(FLAG_WINDOW_ALWAYS_RUN); + + InitWindow(WIDTH * 2, HEIGHT, "MassSprings"); + // Rendering configuration Renderer renderer(WIDTH, HEIGHT); + // Klotski configuration + int current_generator = 0; + MassSpringSystem mass_springs; + State board = apply_state(mass_springs, generators[current_generator]); + // Game loop float frametime; int hov_x = 0; @@ -144,17 +262,28 @@ auto main(int argc, char *argv[]) -> int { } } else if (IsKeyPressed(KEY_P)) { std::cout << board.state << std::endl; + } else if (IsKeyPressed(KEY_N)) { + current_generator = + (generators.size() + current_generator - 1) % generators.size(); + board = apply_state(mass_springs, generators[current_generator]); + } else if (IsKeyPressed(KEY_M)) { + current_generator = (current_generator + 1) % generators.size(); + board = apply_state(mass_springs, generators[current_generator]); + } else if (IsKeyPressed(KEY_R)) { + board = generators[current_generator](); } - if (previous_state != board.state) { - mass_springs.AddMass( - 1.0, - Vector3(static_cast(GetRandomValue(-1000, 1000)) / 1000.0, - static_cast(GetRandomValue(-1000, 1000)) / 1000.0, - static_cast(GetRandomValue(-1000, 1000)) / 1000.0), - false, board.state); - mass_springs.AddSpring(board.state, previous_state, SPRING_CONSTANT, - DAMPENING_CONSTANT, REST_LENGTH); - } + + // Don't need this as long as we're generating the closure beforehand + // if (previous_state != board.state) { + // mass_springs.AddMass( + // 1.0, + // Vector3(static_cast(GetRandomValue(-1000, 1000)) / 1000.0, + // static_cast(GetRandomValue(-1000, 1000)) / 1000.0, + // static_cast(GetRandomValue(-1000, 1000)) / 1000.0), + // false, board.state); + // mass_springs.AddSpring(board.state, previous_state, SPRING_CONSTANT, + // DAMPENING_CONSTANT, REST_LENGTH); + // } // Physics update std::chrono::high_resolution_clock::time_point ps = @@ -184,7 +313,7 @@ auto main(int argc, char *argv[]) -> int { time_measure_count++; if (GetTime() - last_print_time > 10.0) { - std::cout << "\n - Physics time avg: " + std::cout << " - Physics time avg: " << physics_time_accumulator / time_measure_count << "." << std::endl; std::cout << " - Render time avg: " diff --git a/src/renderer.cpp b/src/renderer.cpp index 4f9c9de..a08f3d9 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -123,7 +123,8 @@ auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x, DrawRectangle(0, 0, width, height, RAYWHITE); DrawRectangle(x_offset, y_offset, board_width - 2 * x_offset + 2 * BOARD_PADDING, - board_height - 2 * y_offset + 2 * BOARD_PADDING, LIGHTGRAY); + board_height - 2 * y_offset + 2 * BOARD_PADDING, + state.restricted ? DARKGRAY : LIGHTGRAY); for (int x = 0; x < state.width; ++x) { for (int y = 0; y < state.height; ++y) { DrawRectangle(x_offset + BOARD_PADDING + x * BLOCK_PADDING * 2 +