implement klotski graph closure solving + improve camera controls (panning)

This commit is contained in:
2026-02-18 00:53:42 +01:00
parent 039d96eee3
commit 47fcea6bcb
10 changed files with 226 additions and 96 deletions

View File

@ -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<std::string>{}(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<char>(width + static_cast<int>('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<std::string>{}(state); }
auto State::Hash() const -> int { return std::hash<std::string>{}(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<State> {
std::vector<State> 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::unordered_set<std::string>,
std::vector<std::pair<std::string, std::string>>> {
std::unordered_set<std::string> states;
std::vector<std::pair<std::string, std::string>> links;
std::unordered_set<State> remaining_states;
remaining_states.insert(*this);
do {
const State current = *remaining_states.begin();
remaining_states.erase(current);
std::vector<State> 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);
}

View File

@ -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::unordered_set<std::string>,
std::vector<std::pair<std::string, std::string>>>
closure = board.Closure();
for (const auto &state : closure.first) {
Vector3 pos =
Vector3(static_cast<float>(GetRandomValue(-10000, 10000)) / 1000.0,
static_cast<float>(GetRandomValue(-10000, 10000)) / 1000.0,
static_cast<float>(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<float>(GetRandomValue(-1000, 1000)) / 1000.0,
static_cast<float>(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

View File

@ -1,7 +1,7 @@
#include "mass_springs.hpp"
#include "config.hpp"
#include <cstddef>
#include <format>
#include <raymath.h>
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<std::string>{}(massA) < std::hash<std::string>{}(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);
}

View File

@ -1,6 +1,7 @@
#include "renderer.hpp"
#include <raylib.h>
#include <raymath.h>
#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);