From d92391271feee2ac5a2d7720adbc56ef6e0c2160 Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Wed, 18 Feb 2026 20:27:22 +0100 Subject: [PATCH] add winning conditions and ability to mark them in the graph --- include/config.hpp | 12 ++++--- include/klotski.hpp | 11 ++++-- include/renderer.hpp | 23 ++++++++++--- src/klotski.cpp | 16 ++++++--- src/main.cpp | 80 +++++++++++++++++++++++++++++++++----------- src/renderer.cpp | 61 +++++++++++++++++++++++++++------ 6 files changed, 160 insertions(+), 43 deletions(-) diff --git a/include/config.hpp b/include/config.hpp index 1fe3807..355b431 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -15,26 +15,30 @@ constexpr int MENU_COLS = 3; // Camera Controls constexpr float SIM_SPEED = 4.0; +constexpr float CAMERA_FOV = 120.0; constexpr float CAMERA_DISTANCE = 100.0; constexpr float MIN_CAMERA_DISTANCE = 2.0; constexpr float MAX_CAMERA_DISTANCE = 2000.0; constexpr float ZOOM_SPEED = 2.5; constexpr float ZOOM_MULTIPLIER = 4.0; constexpr float PAN_SPEED = 2.0; +constexpr float PAN_MULTIPLIER = 10.0; constexpr float ROT_SPEED = 1.0; // Physics Engine +constexpr float MASS = 1.0; 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 = 5.0 * REST_LENGTH; -constexpr float VERLET_DAMPENING = 0.01; // [0, 1] +constexpr float REST_LENGTH = 1.1; +constexpr float REPULSION_FORCE = 0.5; +constexpr float REPULSION_RANGE = 3.0 * REST_LENGTH; +constexpr float VERLET_DAMPENING = 0.02; // [0, 1] // Graph Drawing constexpr float VERTEX_SIZE = 0.1; constexpr Color VERTEX_COLOR = GREEN; constexpr Color EDGE_COLOR = DARKGREEN; +constexpr int DRAW_VERTICES_LIMIT = 10000; // Klotski Drawing constexpr int BOARD_PADDING = 5; diff --git a/include/klotski.hpp b/include/klotski.hpp index 85ecc77..f6bc291 100644 --- a/include/klotski.hpp +++ b/include/klotski.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -197,9 +198,9 @@ public: std::cerr << "State width/height must be in [1, 9]!" << std::endl; exit(1); } - if (state.length() != width * height * 2 + 4) { + if (state.length() != width * height * 2 + 5) { std::cerr - << "State representation must have length [width * height * 2 + 4]!" + << "State representation must have length [width * height * 2 + 5]!" << std::endl; exit(1); } @@ -249,6 +250,10 @@ public: auto GetBlock(int x, int y) const -> Block; + auto GetBlockAt(int x, int y) const -> std::string; + + auto GetIndex(int x, int y) const -> int; + auto RemoveBlock(int x, int y) -> bool; auto MoveBlockAt(int x, int y, Direction dir) -> bool; @@ -264,4 +269,6 @@ template <> struct std::hash { std::size_t operator()(const State &s) const noexcept { return s.Hash(); } }; +using WinCondition = std::function; + #endif diff --git a/include/renderer.hpp b/include/renderer.hpp index 7388b26..173977d 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include "config.hpp" #include "klotski.hpp" @@ -26,12 +27,12 @@ private: 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.0), last_mouse(Vector2Zero()), dragging(false), panning(false), target_lock(true) { camera.position = Vector3(0, 0, -1.0 * distance); camera.target = target; camera.up = Vector3(0, 1.0, 0); - camera.fovy = 90.0; + camera.fovy = CAMERA_FOV; camera.projection = CAMERA_PERSPECTIVE; } @@ -47,9 +48,16 @@ private: RenderTexture render_target; RenderTexture klotski_target; RenderTexture menu_target; + std::unordered_set winning_states; public: - Renderer() : camera(OrbitCamera3D(Vector3(0, 0, 0), CAMERA_DISTANCE)) { + bool mark_solutions; + bool connect_solutions; + +public: + Renderer() + : camera(OrbitCamera3D(Vector3(0, 0, 0), CAMERA_DISTANCE)), + mark_solutions(false), connect_solutions(false) { render_target = LoadRenderTexture(WIDTH, HEIGHT); klotski_target = LoadRenderTexture(WIDTH, HEIGHT); menu_target = LoadRenderTexture(WIDTH * 2, MENU_HEIGHT); @@ -67,6 +75,12 @@ public: } public: + auto UpdateWinningStates(const MassSpringSystem &masssprings, + const WinCondition win_condition) -> void; + + auto AddWinningState(const State &state, const WinCondition win_condition) + -> void; + auto UpdateCamera(const MassSpringSystem &masssprings, const State ¤t) -> void; @@ -76,7 +90,8 @@ public: auto DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x, int sel_y) -> void; - auto DrawMenu(const MassSpringSystem &masssprings) -> void; + auto DrawMenu(const MassSpringSystem &masssprings, int current_preset) + -> void; auto DrawTextures() -> void; }; diff --git a/src/klotski.cpp b/src/klotski.cpp index 83f4150..0bbd7c8 100644 --- a/src/klotski.cpp +++ b/src/klotski.cpp @@ -56,7 +56,7 @@ auto State::AddBlock(Block block) -> bool { } } - int index = 5 + (width * block.y + block.x) * 2; + int index = GetIndex(block.x, block.y); state.replace(index, 2, block.ToString()); return true; @@ -76,13 +76,21 @@ auto State::GetBlock(int x, int y) const -> Block { return Block::Invalid(); } +auto State::GetBlockAt(int x, int y) const -> std::string { + return state.substr(GetIndex(x, y), 2); +} + +auto State::GetIndex(int x, int y) const -> int { + return 5 + (y * width + x) * 2; +} + auto State::RemoveBlock(int x, int y) -> bool { - Block b = GetBlock(x, y); - if (!b.IsValid()) { + Block block = GetBlock(x, y); + if (!block.IsValid()) { return false; } - int index = 5 + (width * b.y + b.x) * 2; + int index = GetIndex(block.x, block.y); state.replace(index, 2, ".."); return true; diff --git a/src/main.cpp b/src/main.cpp index 6dd38f2..27cec7f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -22,46 +22,59 @@ auto state_simple_1r() -> State { return s; } +auto state_simple_1r_wc(const State &state) -> bool { return false; } + auto state_simple_1f() -> State { State s = State(4, 5, false); - s.AddBlock(Block(0, 0, 1, 2, true)); + s.AddBlock(Block(0, 0, 1, 2, false)); return s; } +auto state_simple_1f_wc(const State &state) -> bool { return false; } + auto state_simple_2r() -> State { State s = State(4, 5, true); - s.AddBlock(Block(0, 0, 1, 2, true)); + s.AddBlock(Block(0, 0, 1, 2, false)); s.AddBlock(Block(1, 0, 1, 2, false)); return s; } +auto state_simple_2r_wc(const State &state) -> bool { return false; } + auto state_simple_2f() -> State { State s = State(4, 5, false); - s.AddBlock(Block(0, 0, 1, 2, true)); + s.AddBlock(Block(0, 0, 1, 2, false)); s.AddBlock(Block(1, 0, 1, 2, false)); return s; } +auto state_simple_2f_wc(const State &state) -> bool { return false; } + auto state_simple_3r() -> State { State s = State(4, 5, true); - s.AddBlock(Block(0, 0, 1, 2, true)); + s.AddBlock(Block(0, 0, 1, 2, false)); s.AddBlock(Block(1, 0, 1, 2, false)); s.AddBlock(Block(2, 0, 1, 2, false)); return s; } +auto state_simple_3r_wc(const State &state) -> bool { return false; } + auto state_simple_3f() -> State { State s = State(4, 5, false); - s.AddBlock(Block(0, 0, 1, 2, true)); + s.AddBlock(Block(0, 0, 1, 2, false)); s.AddBlock(Block(1, 0, 1, 2, false)); s.AddBlock(Block(2, 0, 1, 2, false)); return s; } + +auto state_simple_3f_wc(const State &state) -> bool { return false; } + auto state_complex_1r() -> State { State s = State(6, 6, true); s.AddBlock(Block(1, 0, 1, 3, false)); @@ -74,6 +87,10 @@ auto state_complex_1r() -> State { return s; } +auto state_complex_1r_wc(const State &state) -> bool { + return state.GetBlockAt(4, 2) == "ba"; +} + auto state_complex_2r() -> State { State s = State(6, 6, true); s.AddBlock(Block(2, 0, 1, 3, false)); @@ -86,6 +103,10 @@ auto state_complex_2r() -> State { return s; } +auto state_complex_2r_wc(const State &state) -> bool { + return state.GetBlockAt(4, 2) == "ba"; +} + auto state_complex_3r() -> State { State s = State(6, 6, true); s.AddBlock(Block(0, 0, 3, 1, false)); @@ -102,12 +123,16 @@ auto state_complex_3r() -> State { return s; } +auto state_complex_3r_wc(const State &state) -> bool { + return state.GetBlockAt(4, 2) == "ba"; +} + 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(1, 1, 2, 2, false)); s.AddBlock(Block(3, 1, 1, 1, false)); s.AddBlock(Block(3, 2, 1, 1, false)); s.AddBlock(Block(0, 3, 1, 1, false)); @@ -116,6 +141,8 @@ auto state_complex_4f() -> State { return s; } +auto state_complex_4f_wc(const State &state) -> bool { return false; } + auto state_klotski() -> State { State s = State(4, 5, false); s.AddBlock(Block(0, 0, 1, 2, false)); @@ -132,17 +159,26 @@ auto state_klotski() -> State { return s; } +auto state_klotski_wc(const State &state) -> bool { + return state.GetBlockAt(1, 3) == "bb"; +} + std::array generators{ state_simple_1r, state_simple_2r, state_simple_3r, state_complex_1r, state_complex_2r, state_complex_3r, state_complex_4f, state_klotski}; +std::array win_conditions{ + state_simple_1r_wc, state_simple_2r_wc, state_simple_3r_wc, + state_complex_1r_wc, state_complex_2r_wc, state_complex_3r_wc, + state_complex_4f_wc, state_klotski_wc}; + auto apply_state(MassSpringSystem &mass_springs, StateGenerator generator) -> State { mass_springs.springs.clear(); mass_springs.masses.clear(); State s = generator(); - mass_springs.AddMass(1.0, Vector3Zero(), false, s.state); + mass_springs.AddMass(MASS, Vector3Zero(), false, s.state); return s; }; @@ -157,7 +193,7 @@ auto solve_closure(MassSpringSystem &mass_springs, const State board) -> void { static_cast(GetRandomValue(-10000, 10000)) / 1000.0, static_cast(GetRandomValue(-10000, 10000)) / 1000.0); - mass_springs.AddMass(1.0, pos, false, state); + mass_springs.AddMass(MASS, pos, false, state); } for (const auto &[from, to] : closure.second) { mass_springs.AddSpring(from, to, SPRING_CONSTANT, DAMPENING_CONSTANT, @@ -196,9 +232,9 @@ auto main(int argc, char *argv[]) -> int { Renderer renderer; // Klotski configuration - int current_generator = 0; + int current_preset = 0; MassSpringSystem masssprings; - State board = apply_state(masssprings, generators[current_generator]); + State board = apply_state(masssprings, generators[current_preset]); // Game loop float frametime; @@ -263,34 +299,40 @@ 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(masssprings, generators[current_generator]); + current_preset = + (generators.size() + current_preset - 1) % generators.size(); + board = apply_state(masssprings, generators[current_preset]); previous_state = board.state; } else if (IsKeyPressed(KEY_M)) { - current_generator = (current_generator + 1) % generators.size(); - board = apply_state(masssprings, generators[current_generator]); + current_preset = (current_preset + 1) % generators.size(); + board = apply_state(masssprings, generators[current_preset]); previous_state = board.state; } else if (IsKeyPressed(KEY_R)) { - board = generators[current_generator](); + board = generators[current_preset](); } else if (IsKeyPressed(KEY_C)) { solve_closure(masssprings, board); + renderer.UpdateWinningStates(masssprings, win_conditions[current_preset]); } else if (IsKeyPressed(KEY_G)) { masssprings.masses.clear(); masssprings.springs.clear(); - masssprings.AddMass(1.0, Vector3Zero(), false, board.state); + masssprings.AddMass(MASS, Vector3Zero(), false, board.state); previous_state = board.state; + } else if (IsKeyPressed(KEY_I)) { + renderer.mark_solutions = !renderer.mark_solutions; + } else if (IsKeyPressed(KEY_O)) { + renderer.connect_solutions = !renderer.connect_solutions; } if (previous_state != board.state) { masssprings.AddMass( - 1.0, + MASS, 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); masssprings.AddSpring(board.state, previous_state, SPRING_CONSTANT, DAMPENING_CONSTANT, REST_LENGTH); + renderer.AddWinningState(board, win_conditions[current_preset]); } // Physics update @@ -314,7 +356,7 @@ auto main(int argc, char *argv[]) -> int { renderer.UpdateCamera(masssprings, board); renderer.DrawMassSprings(masssprings, board); renderer.DrawKlotski(board, hov_x, hov_y, sel_x, sel_y); - renderer.DrawMenu(masssprings); + renderer.DrawMenu(masssprings, current_preset); renderer.DrawTextures(); std::chrono::high_resolution_clock::time_point re = std::chrono::high_resolution_clock::now(); diff --git a/src/renderer.cpp b/src/renderer.cpp index 52f0b22..c00d9cf 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -5,6 +5,7 @@ #include #include "config.hpp" +#include "klotski.hpp" #include "mass_springs.hpp" auto OrbitCamera3D::Update(const Mass ¤t_mass) -> void { @@ -45,7 +46,13 @@ auto OrbitCamera3D::Update(const Mass ¤t_mass) -> void { Vector2 dx = Vector2Subtract(mouse, last_mouse); last_mouse = mouse; - float speed = distance * PAN_SPEED / 1000.0; + // float speed = PAN_SPEED; + float speed; + if (IsKeyDown(KEY_LEFT_SHIFT)) { + speed = distance * PAN_SPEED / 1000.0 * PAN_MULTIPLIER; + } else { + speed = distance * PAN_SPEED / 1000.0; + } Vector3 forward = Vector3Normalize(Vector3Subtract(camera.target, camera.position)); Vector3 right = Vector3Normalize(Vector3CrossProduct(forward, camera.up)); @@ -80,6 +87,27 @@ auto OrbitCamera3D::Update(const Mass ¤t_mass) -> void { camera.target = target; } +auto Renderer::UpdateWinningStates(const MassSpringSystem &masssprings, + const WinCondition win_condition) -> void { + winning_states.clear(); + winning_states.reserve(masssprings.masses.size()); + for (const auto &[state, mass] : masssprings.masses) { + if (win_condition(state)) { + winning_states.insert(state); + } + } + + std::cout << "Found " << winning_states.size() << " winning states." + << std::endl; +} + +auto Renderer::AddWinningState(const State &state, + const WinCondition win_condition) -> void { + if (win_condition(state)) { + winning_states.insert(state); + } +} + auto Renderer::UpdateCamera(const MassSpringSystem &masssprings, const State ¤t) -> void { const Mass &c = masssprings.masses.at(current.state); @@ -102,16 +130,25 @@ auto Renderer::DrawMassSprings(const MassSpringSystem &masssprings, } // Draw masses (high performance impact) - if (masssprings.masses.size() <= 5000) { - for (const auto &[state, mass] : masssprings.masses) { + for (const auto &[state, mass] : masssprings.masses) { + if (state == current.state) { + DrawCube(mass.position, 4 * VERTEX_SIZE, 4 * VERTEX_SIZE, 4 * VERTEX_SIZE, + RED); + } else if (winning_states.contains(state)) { + if (mark_solutions) { + DrawCube(mass.position, 4 * VERTEX_SIZE, 4 * VERTEX_SIZE, + 4 * VERTEX_SIZE, BLUE); + } + if (connect_solutions) { + DrawLine3D(mass.position, masssprings.masses.at(current.state).position, + PURPLE); + } + } else if (masssprings.masses.size() <= DRAW_VERTICES_LIMIT) { DrawCube(mass.position, VERTEX_SIZE, VERTEX_SIZE, VERTEX_SIZE, VERTEX_COLOR); } } - const Mass &c = masssprings.masses.at(current.state); - DrawCube(c.position, 2 * VERTEX_SIZE, 2 * VERTEX_SIZE, 2 * VERTEX_SIZE, RED); - // DrawGrid(10, 1.0); // DrawSphere(camera.target, VERTEX_SIZE, ORANGE); EndMode3D(); @@ -203,7 +240,8 @@ auto Renderer::DrawKlotski(const State &state, int hov_x, int hov_y, int sel_x, EndTextureMode(); } -auto Renderer::DrawMenu(const MassSpringSystem &masssprings) -> void { +auto Renderer::DrawMenu(const MassSpringSystem &masssprings, int current_preset) + -> void { BeginTextureMode(menu_target); ClearBackground(RAYWHITE); @@ -237,10 +275,13 @@ auto Renderer::DrawMenu(const MassSpringSystem &masssprings) -> void { DARKGREEN); draw_btn(1, 0, std::format("Reset Board State (R)"), DARKBLUE); - draw_btn(1, 1, std::format("Switch to Next Preset (M)"), DARKBLUE); - draw_btn(1, 2, std::format("Switch to Previous Preset (N)"), DARKBLUE); + draw_btn(1, 1, std::format("Preset (M/N): {}", current_preset), DARKBLUE); + draw_btn(1, 2, std::format("Print Board State to Console (P)"), DARKBLUE); - draw_btn(2, 0, std::format("Print Board State to Console (P)"), DARKPURPLE); + draw_btn(2, 0, + std::format("Mark (I): {} / Connect (O): {}", mark_solutions, + connect_solutions), + DARKPURPLE); draw_btn(2, 1, std::format("Solve Board Closure (C)"), DARKPURPLE); draw_btn(2, 2, std::format("Clear Graph (G)"), DARKPURPLE);