From fd58f217c6963b30ae0bd094fc076d1032a4441b Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Wed, 25 Feb 2026 01:15:47 +0100 Subject: [PATCH] implement bfs multi-target distance calculation to nearest winning state --- CMakeLists.txt | 1 + include/distance.hpp | 32 +++++++++++++++ include/physics.hpp | 4 +- include/renderer.hpp | 8 +--- include/state.hpp | 18 +++++++++ src/distance.cpp | 92 ++++++++++++++++++++++++++++++++++++++++++++ src/input.cpp | 1 + src/main.cpp | 15 ++------ src/physics.cpp | 6 --- src/renderer.cpp | 12 ++---- src/state.cpp | 56 +++++++++++++++++++++++++-- 11 files changed, 208 insertions(+), 37 deletions(-) create mode 100644 include/distance.hpp create mode 100644 src/distance.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index cca3dda..60df08b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,7 @@ add_executable(masssprings src/input.cpp src/tracy.cpp src/backward.cpp + src/distance.cpp ) target_include_directories(masssprings PUBLIC ${RAYLIB_CPP_INCLUDE_DIR}) diff --git a/include/distance.hpp b/include/distance.hpp new file mode 100644 index 0000000..38237eb --- /dev/null +++ b/include/distance.hpp @@ -0,0 +1,32 @@ +#ifndef __DISTANCE_HPP_ +#define __DISTANCE_HPP_ + +#include "config.hpp" + +#include +#include + +struct DistanceResult { + // distances[n] = distance from n to target + std::vector distances; + + // parents[n] = next node on the path from n to target + std::vector parents; + + // nearest_target[n] = closest target node to n + std::vector nearest_targets; + + auto Clear() -> void; + + auto Empty() -> bool; +}; + +auto CalculateDistances( + std::size_t node_count, + const std::vector> &edges, + const std::vector &targets) -> DistanceResult; + +auto GetPath(const DistanceResult &result, std::size_t source) + -> std::vector; + +#endif diff --git a/include/physics.hpp b/include/physics.hpp index 7d9e1fc..e969663 100644 --- a/include/physics.hpp +++ b/include/physics.hpp @@ -141,11 +141,9 @@ class ThreadedPhysics { std::condition_variable_any data_ready_cnd; std::condition_variable_any data_consumed_cnd; unsigned int ups = 0; + std::vector masses; // Read by renderer bool data_ready = false; bool data_consumed = true; - std::vector masses; // Read by renderer - std::vector> - springs; // Read by renderer std::atomic running{true}; }; diff --git a/include/renderer.hpp b/include/renderer.hpp index f9dfa34..fecf104 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -67,15 +67,11 @@ private: public: auto UpdateTextureSizes() -> void; - auto DrawMassSprings( - const std::vector &masses, - const std::vector> &springs) -> void; + auto DrawMassSprings(const std::vector &masses) -> void; auto DrawKlotski() -> void; - auto DrawMenu(const std::vector &masses, - const std::vector> &springs) - -> void; + auto DrawMenu(const std::vector &masses) -> void; auto DrawTextures(float ups) -> void; }; diff --git a/include/state.hpp b/include/state.hpp index f0a1ecf..a88fa24 100644 --- a/include/state.hpp +++ b/include/state.hpp @@ -2,6 +2,7 @@ #define __STATE_HPP_ #include "config.hpp" +#include "distance.hpp" #include "physics.hpp" #include "puzzle.hpp" @@ -15,10 +16,23 @@ public: std::vector presets; + // Some stuff is faster to map from state to mass (e.g. in the renderer) std::unordered_map states; std::unordered_set winning_states; std::unordered_set visited_states; + // Other stuff maps from mass to state :/ + std::unordered_map masses; + std::vector winning_path; + + // Fuck it, duplicate the springs too, we don't even need to copy them from + // the physics thread then... + std::vector> springs; + + // Distance calculation result can be buffered and reused to calculate a new + // path on the same graph + DistanceResult target_distances; + int current_preset; State starting_state; State current_state; @@ -62,6 +76,10 @@ public: auto FindWinningStates() -> void; + auto FindTargetDistances() -> void; + + auto FindTargetPath() -> void; + auto CurrentMassIndex() const -> std::size_t; }; diff --git a/src/distance.cpp b/src/distance.cpp new file mode 100644 index 0000000..977b586 --- /dev/null +++ b/src/distance.cpp @@ -0,0 +1,92 @@ +#include "distance.hpp" +#include "config.hpp" + +#include +#include +#include + +#ifdef TRACY +#include "tracy.hpp" +#include +#endif + +auto DistanceResult::Clear() -> void { + distances.clear(); + parents.clear(); + nearest_targets.clear(); +} + +auto DistanceResult::Empty() -> bool { + return distances.empty() || parents.empty() || nearest_targets.empty(); +} + +auto CalculateDistances( + std::size_t node_count, + const std::vector> &edges, + const std::vector &targets) -> DistanceResult { + + // Build a list of adjacent nodes to speed up BFS + std::vector> adjacency(node_count); + for (const auto &[from, to] : edges) { + adjacency[from].push_back(to); + adjacency[to].push_back(from); + } + // for (size_t i = 0; i < adjacency.size(); ++i) { + // std::cout << "Node " << i << "'s neighbors: "; + // for (const auto &neighbor : adjacency[i]) { + // std::cout << neighbor; + // } + // std::cout << "\n"; + // } + // std::cout << std::endl; + + std::vector distances(node_count, -1); + std::vector parents(node_count, -1); + std::vector nearest_targets(node_count, -1); + + std::queue queue; + for (std::size_t target : targets) { + distances[target] = 0; + nearest_targets[target] = target; + queue.push(target); + } + + while (!queue.empty()) { + std::size_t current = queue.front(); + queue.pop(); + + for (std::size_t neighbor : adjacency[current]) { + // std::cout << "Visiting edge (" << current << "->" << neighbor << ")." + // << std::endl; + if (distances[neighbor] == -1) { + // If distance is -1 we haven't visited the node yet + distances[neighbor] = distances[current] + 1; + parents[neighbor] = current; + nearest_targets[neighbor] = nearest_targets[current]; + // std::cout << "- Distance: " << distances[neighbor] + // << ", Parent: " << parents[neighbor] + // << ", Nearest Target: " << nearest_targets[neighbor] << "." + // << std::endl; + queue.push(neighbor); + } + } + } + + return {distances, parents, nearest_targets}; +} + +auto GetPath(const DistanceResult &result, std::size_t source) + -> std::vector { + if (result.distances[source] == -1) { + // Unreachable + return {}; + } + + std::vector path; + for (std::size_t n = source; n != static_cast(-1); + n = result.parents[n]) { + path.push_back(n); + } + + return path; +} diff --git a/src/input.cpp b/src/input.cpp index 9335bcd..35b0fd0 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -1,5 +1,6 @@ #include "input.hpp" #include "config.hpp" +#include "distance.hpp" #include #include diff --git a/src/main.cpp b/src/main.cpp index 9fbc6fd..53baec3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -3,6 +3,7 @@ #include #include "config.hpp" +#include "distance.hpp" #include "input.hpp" #include "physics.hpp" #include "renderer.hpp" @@ -13,12 +14,6 @@ #include #endif -// TODO: Klotski state file loading -// - File should contain a single state per line, multiple lines possible -// - If a file is loaded, the presets should be replaced with the states -// from the file -// - Automatically determine the winning condition based on a configured -// board exit // TODO: Graph interaction // - Click states to display them in the board // - Find shortest path to any winning state and mark it in the graph @@ -48,8 +43,7 @@ auto main(int argc, char *argv[]) -> int { Renderer renderer(camera, state, input); unsigned int ups; - std::vector masses; // Read from physics - std::vector> springs; // Read from physics + std::vector masses; // Read from physics // Game loop while (!WindowShouldClose()) { @@ -78,7 +72,6 @@ auto main(int argc, char *argv[]) -> int { // Only copy data if any has been produced if (physics.state.data_ready) { masses = physics.state.masses; - springs = physics.state.springs; physics.state.data_ready = false; physics.state.data_consumed = true; @@ -101,9 +94,9 @@ auto main(int argc, char *argv[]) -> int { // Rendering renderer.UpdateTextureSizes(); - renderer.DrawMassSprings(masses, springs); + renderer.DrawMassSprings(masses); renderer.DrawKlotski(); - renderer.DrawMenu(masses, springs); + renderer.DrawMenu(masses); renderer.DrawTextures(ups); #ifdef TRACY FrameMark; diff --git a/src/physics.cpp b/src/physics.cpp index 199e6ab..648809a 100644 --- a/src/physics.cpp +++ b/src/physics.cpp @@ -288,12 +288,6 @@ auto ThreadedPhysics::PhysicsThread(ThreadedPhysics::PhysicsState &state) state.masses.emplace_back(mass.position); } - state.springs.clear(); - state.springs.reserve(mass_springs.springs.size()); - for (const auto &spring : mass_springs.springs) { - state.springs.emplace_back(spring.a, spring.b); - } - state.data_ready = true; state.data_consumed = false; } diff --git a/src/renderer.cpp b/src/renderer.cpp index 1d0d260..dcf97b7 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -59,9 +59,7 @@ auto Renderer::ReallocateGraphInstancingIfNecessary(std::size_t size) -> void { } } -auto Renderer::DrawMassSprings( - const std::vector &masses, - const std::vector> &springs) -> void { +auto Renderer::DrawMassSprings(const std::vector &masses) -> void { #ifdef TRACY ZoneScoped; #endif @@ -96,7 +94,7 @@ auto Renderer::DrawMassSprings( ZoneNamedN(draw_springs, "DrawSprings", true); #endif rlBegin(RL_LINES); - for (const auto &[from, to] : springs) { + for (const auto &[from, to] : state.springs) { if (masses.size() > from && masses.size() > to) { const Vector3 &a = masses.at(from); const Vector3 &b = masses.at(to); @@ -289,9 +287,7 @@ auto Renderer::DrawKlotski() -> void { EndTextureMode(); } -auto Renderer::DrawMenu( - const std::vector &masses, - const std::vector> &springs) -> void { +auto Renderer::DrawMenu(const std::vector &masses) -> void { #ifdef TRACY ZoneScoped; #endif @@ -317,7 +313,7 @@ auto Renderer::DrawMenu( draw_btn(0, 0, std::format("States: {}, Transitions: {}, Winning: {}", - masses.size(), springs.size(), + masses.size(), state.springs.size(), state.winning_states.size()), DARKGREEN); draw_btn( diff --git a/src/state.cpp b/src/state.cpp index 89664e6..f037a2d 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -68,6 +68,8 @@ auto StateManager::NextPreset() -> void { } auto StateManager::FillGraph() -> void { + ZoneScoped; + ClearGraph(); std::pair, @@ -78,8 +80,14 @@ auto StateManager::FillGraph() -> void { physics.AddMassSpringsCmd(closure.first.size(), closure.second); for (const State &state : closure.first) { states.insert(std::make_pair(state, states.size())); + masses.insert(std::make_pair(masses.size(), state)); + } + for (const auto &[from, to] : closure.second) { + springs.emplace_back(from, to); } FindWinningStates(); + FindTargetDistances(); + FindTargetPath(); } auto StateManager::UpdateGraph() -> void { @@ -89,23 +97,34 @@ auto StateManager::UpdateGraph() -> void { if (!states.contains(current_state)) { states.insert(std::make_pair(current_state, states.size())); + masses.insert(std::make_pair(masses.size(), current_state)); + springs.emplace_back(states.at(current_state), states.at(previous_state)); physics.AddMassCmd(); physics.AddSpringCmd(states.at(current_state), states.at(previous_state)); + + if (current_state.IsWon()) { + winning_states.insert(current_state); + } + FindTargetDistances(); } visited_states.insert(current_state); - if (current_state.IsWon()) { - winning_states.insert(current_state); - } + FindTargetPath(); } auto StateManager::ClearGraph() -> void { states.clear(); winning_states.clear(); visited_states.clear(); + masses.clear(); + winning_path.clear(); + springs.clear(); + target_distances.Clear(); physics.ClearCmd(); + // Re-add the default stuff to the graph states.insert(std::make_pair(current_state, states.size())); + masses.insert(std::make_pair(masses.size(), current_state)); visited_states.insert(current_state); physics.AddMassCmd(); @@ -123,6 +142,37 @@ auto StateManager::FindWinningStates() -> void { } } +auto StateManager::FindTargetDistances() -> void { + ZoneScoped; + + if (springs.size() == 0 || winning_states.size() == 0) { + return; + } + + // Find target indices + std::vector targets; + targets.reserve(winning_states.size()); + for (const auto &_state : winning_states) { + targets.push_back(states.at(_state)); + } + + target_distances = CalculateDistances(states.size(), springs, targets); + + std::cout << "Calculated " << target_distances.distances.size() + << " distances to " << targets.size() << " targets." << std::endl; +} + +auto StateManager::FindTargetPath() -> void { + if (target_distances.Empty()) { + return; + } + + winning_path = GetPath(target_distances, CurrentMassIndex()); + + std::cout << "Nearest target is " << winning_path.size() << " moves away." + << std::endl; +} + auto StateManager::CurrentMassIndex() const -> std::size_t { return states.at(current_state); }