From d8534cacdd47898c409f540bf3ca89c4d83a24c9 Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Tue, 24 Feb 2026 21:33:05 +0100 Subject: [PATCH] store winning condition in state + remove presets --- CMakeLists.txt | 3 + default.puzzle | 32 +++++++ flake.nix | 5 +- include/config.hpp | 4 +- include/presets.hpp | 215 -------------------------------------------- include/puzzle.hpp | 68 ++++++++++---- include/state.hpp | 18 ++-- src/main.cpp | 12 +-- src/puzzle.cpp | 42 ++++++--- src/renderer.cpp | 2 +- src/state.cpp | 56 ++++++++---- 11 files changed, 178 insertions(+), 279 deletions(-) create mode 100644 default.puzzle delete mode 100644 include/presets.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index be16d57..cca3dda 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,9 @@ if(USE_TRACY) FetchContent_MakeAvailable(tracy) option(TRACY_ENABLE "" ON) option(TRACY_ON_DEMAND "" ON) + + # Enable tracy macros in the application + add_compile_definitions(TRACY) endif() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -Wfloat-equal -Wundef -Wshadow -Wpointer-arith -Wcast-align -Wno-unused-parameter -Wunreachable-code") diff --git a/default.puzzle b/default.puzzle new file mode 100644 index 0000000..54559cb --- /dev/null +++ b/default.puzzle @@ -0,0 +1,32 @@ +// 1 Block, 1 Axis, no goal +R4599ab...................................... + +// 2 Blocks, 2 Axes, no goal +R45991212.................................... + +// 3 Blocks, 3 Axes, still no goal +R4599121212.................................. + +// Square with no goal +F449921....111222..11......111111.... + +// RushHour 1 +R6642..13..21..13..................ba..........12............21.............. + +// RushHour 2 +R664231........13................12ba..........1221..12..12..21........31.... + +// RushHour 3 +R66421231....1213..31........ba..121212..21................1221..........21.. + +// RushHour 4 +R664231....12....1221......12..ba..1212..21..12....12......21......21..21.... + +// Klotski +F451312bb..12........1221..12..1111..11....11 + +// Century +F451311bb..1112....12..12....11....1121..21.. + +// Super Century +F451312111111..12bb..12........21..11....21.. diff --git a/flake.nix b/flake.nix index e0c15be..ddcf235 100644 --- a/flake.nix +++ b/flake.nix @@ -172,7 +172,7 @@ rec { package = stdenv.mkDerivation rec { inherit buildInputs; pname = "masssprings"; - version = "0.0.1"; + version = "1.0.0"; src = ./.; nativeBuildInputs = with pkgs; [ @@ -182,7 +182,8 @@ rec { installPhase = '' mkdir -p $out/bin - mv ./${pname} $out/bin + cp ./${pname} $out/bin/ + cp $src/default.puzzle $out/bin/ ''; }; in rec { diff --git a/include/config.hpp b/include/config.hpp index dda39a1..0a0aa79 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -4,9 +4,11 @@ #include #define THREADPOOL // Enable physics threadpool -#define TRACY // Enable tracy profiling support #define BACKWARD // Enable pretty stack traces +// Gets set by CMake +// #define TRACY // Enable tracy profiling support + // Window constexpr int INITIAL_WIDTH = 800; constexpr int INITIAL_HEIGHT = 800; diff --git a/include/presets.hpp b/include/presets.hpp deleted file mode 100644 index c9881e3..0000000 --- a/include/presets.hpp +++ /dev/null @@ -1,215 +0,0 @@ -#ifndef __PRESETS_HPP_ -#define __PRESETS_HPP_ - -#include "config.hpp" -#include "puzzle.hpp" - -#include -#include - -using StateGenerator = std::function; - -inline auto state_simple_1r() -> State { - State s = State(4, 5, true); - s.AddBlock(Block(0, 0, 1, 2, true)); - - return s; -} - -inline auto state_simple_1r_wc(const State &state) -> bool { return false; } - -inline auto state_simple_1f() -> State { - State s = State(4, 5, false); - s.AddBlock(Block(0, 0, 1, 2, false)); - - return s; -} - -inline auto state_simple_1f_wc(const State &state) -> bool { return false; } - -inline auto state_simple_2r() -> State { - State s = State(4, 5, true); - s.AddBlock(Block(0, 0, 1, 2, false)); - s.AddBlock(Block(1, 0, 1, 2, false)); - - return s; -} - -inline auto state_simple_2r_wc(const State &state) -> bool { return false; } - -inline auto state_simple_2f() -> State { - State s = State(4, 5, false); - s.AddBlock(Block(0, 0, 1, 2, false)); - s.AddBlock(Block(1, 0, 1, 2, false)); - - return s; -} - -inline auto state_simple_2f_wc(const State &state) -> bool { return false; } - -inline auto state_simple_3r() -> State { - State s = State(4, 5, 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; -} - -inline auto state_simple_3r_wc(const State &state) -> bool { return false; } - -inline auto state_simple_3f() -> State { - State s = State(4, 5, false); - 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; -} - -inline auto state_simple_3f_wc(const State &state) -> bool { return false; } - -inline 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; -} - -inline auto state_complex_1r_wc(const State &state) -> bool { - return state.GetBlockAt(4, 2) == "ba"; -} - -inline 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; -} - -inline auto state_complex_2r_wc(const State &state) -> bool { - return state.GetBlockAt(4, 2) == "ba"; -} - -inline 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; -} - -inline auto state_complex_3r_wc(const State &state) -> bool { - return state.GetBlockAt(4, 2) == "ba"; -} - -inline 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, 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)); - s.AddBlock(Block(1, 3, 1, 1, false)); - - return s; -} - -inline auto state_complex_4f_wc(const State &state) -> bool { return false; } - -inline auto state_complex_5r() -> State { - return State("R6x6:31....12....1221......12..ba..1212..21..12....12......21.." - "....21..21...."); -} - -inline auto state_complex_5r_wc(const State &state) -> bool { - return state.GetBlockAt(4, 2) == "ba"; -} - -inline auto state_complex_6r() -> State { - return State( - "R6x6:1231....1213..31........ba..121212..21................1221....." - ".....21.."); -} - -inline auto state_complex_6r_wc(const State &state) -> bool { - return state.GetBlockAt(4, 2) == "ba"; -} - -inline 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)); - s.AddBlock(Block(0, 2, 1, 2, false)); - s.AddBlock(Block(1, 2, 2, 1, false)); - s.AddBlock(Block(3, 2, 1, 2, false)); - s.AddBlock(Block(1, 3, 1, 1, false)); - s.AddBlock(Block(2, 3, 1, 1, false)); - s.AddBlock(Block(0, 4, 1, 1, false)); - s.AddBlock(Block(3, 4, 1, 1, false)); - - return s; -} - -inline auto state_klotski_wc(const State &state) -> bool { - return state.GetBlockAt(1, 3) == "bb"; -} - -inline auto state_century() -> State { - return State("F4x5:11bb..1112....12..12....11....1121..21.."); -} - -inline auto state_century_wc(const State &state) -> bool { - return state.GetBlockAt(1, 3) == "bb"; -} - -inline auto state_super_century() -> State { - return State("F4x5:12111111..12bb..12........21..11....21.."); -} - -inline auto state_super_century_wc(const State &state) -> bool { - return state.GetBlockAt(1, 3) == "bb"; -} - -inline auto state_new_century() -> State { - return State("F4x5:12111111..12bb..12........21..11....21.."); -} - -inline auto state_new_century_wc(const State &state) -> bool { - // What kind of brain do you need for this??? - return state.state == "F4x5:21......1121..12bb..12........12111111.."; -} - -static std::vector generators{ - state_simple_1r, state_simple_2r, state_simple_3r, state_complex_1r, - state_complex_2r, state_complex_3r, state_complex_4f, state_complex_5r, - state_complex_6r, state_klotski, state_century, state_super_century}; - -static std::vector 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_complex_5r_wc, state_complex_6r_wc, - state_klotski_wc, state_century_wc, state_super_century_wc}; - -#endif diff --git a/include/puzzle.hpp b/include/puzzle.hpp index b4b0a9a..eb1ce38 100644 --- a/include/puzzle.hpp +++ b/include/puzzle.hpp @@ -102,16 +102,21 @@ public: auto Collides(const Block &other) const -> bool; }; -// A state is represented by a string "WxH:blocks", where W is the board width, -// H is the board height and blocks is an enumeration of each of the board's -// cells, with each cell being a 2-letter or 2-digit block representation (a 3x3 -// board would have a string representation with length 4 + 3*3 * 2). -// The board's cells are enumerated from top-left to bottom-right with each -// block's pivot being its top-left corner. +// A state is represented by a string "MWHXYblocks", where M is "R" +// (restricted) or "F" (free), W is the board width, H is the board height, X is +// the target block x goal, Y is the target block y goal and blocks is an +// enumeration of each of the board's cells, with each cell being a 2-letter or +// 2-digit block representation (a 3x3 board would have a string representation +// with length 5 + 3*3 * 2). The board's cells are enumerated from top-left to +// bottom-right with each block's pivot being its top-left corner. class State { public: + static constexpr int prefix = 5; + int width; int height; + int target_x; + int target_y; bool restricted; // Only allow blocks to move in their principal direction std::string state; @@ -133,13 +138,13 @@ public: Block operator*() const { return Block(current_pos % state.width, current_pos / state.width, - state.state.substr(current_pos * 2 + 5, 2)); + state.state.substr(current_pos * 2 + prefix, 2)); } BlockIterator &operator++() { do { current_pos++; - } while (state.state.substr(current_pos * 2 + 5, 2) == ".."); + } while (state.state.substr(current_pos * 2 + prefix, 2) == ".."); return *this; } @@ -151,28 +156,51 @@ public: }; public: - 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) { + State(int _width, int _height, int _target_x, int _target_y, bool _restricted) + : width(_width), height(_height), target_x(_target_x), + target_y(_target_y), restricted(_restricted), + state(std::format("{}{}{}{}{}{}", _restricted ? "R" : "F", _width, + _height, _target_x, _target_y, + std::string(_width * _height * 2, '.'))) { + if (_width < 1 || _width > 9 || _height < 1 || _height > 9) { std::cerr << "State width/height must be in [1, 9]!" << std::endl; exit(1); } + if (_target_x < 0 || _target_x >= 9 || _target_y < 0 || _target_y >= 9) { + if (_target_x != 9 && _target_y != 9) { + std::cerr << "State target must be within the board bounds!" + << std::endl; + exit(1); + } + } } + State(int _width, int _height, bool _restricted) + : State(_width, _height, 9, 9, _restricted) {} + + State() : State(4, 5, 9, 9, false) {} + explicit State(std::string _state) : width(std::stoi(_state.substr(1, 1))), - height(std::stoi(_state.substr(3, 1))), + height(std::stoi(_state.substr(2, 1))), + target_x(std::stoi(_state.substr(3, 1))), + target_y(std::stoi(_state.substr(4, 1))), restricted(_state.substr(0, 1) == "R"), state(_state) { - if (width <= 0 || width >= 10 || height <= 0 || height >= 10) { + if (width < 1 || width > 9 || height < 1 || height > 9) { std::cerr << "State width/height must be in [1, 9]!" << std::endl; exit(1); } - if (static_cast(_state.length()) != width * height * 2 + 5) { + if (target_x < 0 || target_x >= 9 || target_y < 0 || target_y >= 9) { + if (target_x != 9 && target_y != 9) { + std::cerr << "State target must be within the board bounds!" + << std::endl; + exit(1); + } + } + if (static_cast(_state.length()) != width * height * 2 + prefix) { std::cerr - << "State representation must have length [width * height * 2 + 5]!" - << std::endl; + << "State representation must have length [width * height * 2 + " + << prefix << "]!" << std::endl; exit(1); } } @@ -194,6 +222,10 @@ public: public: auto Hash() const -> int; + auto HasWinCondition() const -> bool; + + auto IsWon() const -> bool; + auto AddColumn() const -> State; auto RemoveColumn() const -> State; diff --git a/include/state.hpp b/include/state.hpp index 7a3c814..f0a1ecf 100644 --- a/include/state.hpp +++ b/include/state.hpp @@ -3,7 +3,6 @@ #include "config.hpp" #include "physics.hpp" -#include "presets.hpp" #include "puzzle.hpp" #include @@ -14,6 +13,8 @@ class StateManager { public: ThreadedPhysics &physics; + std::vector presets; + std::unordered_map states; std::unordered_set winning_states; std::unordered_set visited_states; @@ -26,11 +27,11 @@ public: bool edited = false; public: - StateManager(ThreadedPhysics &_physics) - : physics(_physics), current_preset(0), - starting_state(generators[current_preset]()), - current_state(starting_state), previous_state(starting_state), + StateManager(ThreadedPhysics &_physics, const std::string &preset_file) + : physics(_physics), presets({State()}), current_preset(0), edited(false) { + ParsePresetFile(preset_file); + current_state = presets.at(current_preset); ClearGraph(); } @@ -41,6 +42,9 @@ public: ~StateManager() {} +private: + auto ParsePresetFile(const std::string &preset_file) -> void; + public: auto LoadPreset(int preset) -> void; @@ -58,10 +62,6 @@ public: auto FindWinningStates() -> void; - auto CurrentGenerator() const -> StateGenerator; - - auto CurrentWinCondition() const -> WinCondition; - auto CurrentMassIndex() const -> std::size_t; }; diff --git a/src/main.cpp b/src/main.cpp index 775247a..9fbc6fd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,10 +25,12 @@ // - Also mark the next move along the path on the board auto main(int argc, char *argv[]) -> int { - // if (argc < 2) { - // std::cout << "Missing .klotski file." << std::endl; - // return 1; - // } + std::string preset_file; + if (argc != 2) { + preset_file = "default.puzzle"; + } else { + preset_file = argv[1]; + } // RayLib window setup SetTraceLogLevel(LOG_ERROR); @@ -40,7 +42,7 @@ auto main(int argc, char *argv[]) -> int { // Game setup ThreadedPhysics physics; - StateManager state(physics); + StateManager state(physics, preset_file); InputHandler input(state); OrbitCamera3D camera; Renderer renderer(camera, state, input); diff --git a/src/puzzle.cpp b/src/puzzle.cpp index cf58046..9f5d6e9 100644 --- a/src/puzzle.cpp +++ b/src/puzzle.cpp @@ -53,6 +53,24 @@ auto Block::Collides(const Block &other) const -> bool { auto State::Hash() const -> int { return std::hash{}(state); } +auto State::HasWinCondition() const -> bool { + return target_x == 9 || target_y == 9; +} + +auto State::IsWon() const -> bool { + if (!HasWinCondition()) { + return false; + } + + for (const auto &block : *this) { + if (block.target) { + return block.x == target_x && block.y == target_y; + } + } + + return false; +} + auto State::AddColumn() const -> State { State newstate = State(width + 1, height, restricted); @@ -129,7 +147,7 @@ auto State::GetBlockAt(int x, int y) const -> std::string { } auto State::GetIndex(int x, int y) const -> int { - return 5 + (y * width + x) * 2; + return prefix + (y * width + x) * 2; } auto State::RemoveBlock(int x, int y) -> bool { @@ -185,36 +203,36 @@ auto State::MoveBlockAt(int x, int y, Direction dir) -> bool { Direction::WES; // Get target block - int target_x = block.x; - int target_y = block.y; + int _target_x = block.x; + int _target_y = block.y; switch (dir) { case Direction::NOR: - if (!(dirs & Direction::NOR) || target_y < 1) { + if (!(dirs & Direction::NOR) || _target_y < 1) { return false; } - target_y--; + _target_y--; break; case Direction::EAS: - if (!(dirs & Direction::EAS) || target_x + block.width >= width) { + if (!(dirs & Direction::EAS) || _target_x + block.width >= width) { return false; } - target_x++; + _target_x++; break; case Direction::SOU: - if (!(dirs & Direction::SOU) || target_y + block.height >= height) { + if (!(dirs & Direction::SOU) || _target_y + block.height >= height) { return false; } - target_y++; + _target_y++; break; case Direction::WES: - if (!(dirs & Direction::WES) || target_x < 1) { + if (!(dirs & Direction::WES) || _target_x < 1) { return false; } - target_x--; + _target_x--; break; } Block target = - Block(target_x, target_y, block.width, block.height, block.target); + Block(_target_x, _target_y, block.width, block.height, block.target); // Check collisions for (Block b : *this) { diff --git a/src/renderer.cpp b/src/renderer.cpp index efb0165..c95e560 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -207,7 +207,7 @@ auto Renderer::DrawKlotski() -> void { DrawRectangle(x_offset, y_offset, board_width - 2 * x_offset + 2 * BOARD_PADDING, board_height - 2 * y_offset + 2 * BOARD_PADDING, - state.CurrentWinCondition()(state.current_state) + state.current_state.IsWon() ? GREEN : (state.current_state.restricted ? DARKGRAY : LIGHTGRAY)); for (int x = 0; x < state.current_state.width; ++x) { diff --git a/src/state.cpp b/src/state.cpp index fc52a99..89664e6 100644 --- a/src/state.cpp +++ b/src/state.cpp @@ -1,7 +1,8 @@ #include "state.hpp" #include "config.hpp" -#include "presets.hpp" +#include +#include #include #ifdef TRACY @@ -9,30 +10,61 @@ #include #endif +auto StateManager::ParsePresetFile(const std::string &preset_file) -> void { + std::ifstream file(preset_file); + if (!file) { + std::cout << "Preset file \"" << preset_file << "\" couldn't be loaded." + << std::endl; + return; + } + + std::string line; + std::vector lines; + while (std::getline(file, line)) { + if (line.starts_with("F") || line.starts_with("R")) { + lines.push_back(line); + } + } + + if (lines.size() == 0) { + std::cout << "Preset file \"" << preset_file << "\" couldn't be loaded." + << std::endl; + return; + } + + presets.clear(); + for (const auto &preset : lines) { + presets.emplace_back(preset); + } + + std::cout << "Loaded " << lines.size() << " presets." << std::endl; +} + auto StateManager::LoadPreset(int preset) -> void { current_preset = preset; - current_state = CurrentGenerator()(); + current_state = presets.at(current_preset); ClearGraph(); edited = false; } auto StateManager::ResetState() -> void { - current_state = CurrentGenerator()(); + current_state = presets.at(current_preset); previous_state = current_state; if (edited) { // We also need to clear the graph in case the state has been edited - // because the graph could contain states that are impossible to reach now. + // because the graph could contain states that are impossible to reach + // now. ClearGraph(); edited = false; } } auto StateManager::PreviousPreset() -> void { - LoadPreset((generators.size() + current_preset - 1) % generators.size()); + LoadPreset((presets.size() + current_preset - 1) % presets.size()); } auto StateManager::NextPreset() -> void { - LoadPreset((current_preset + 1) % generators.size()); + LoadPreset((current_preset + 1) % presets.size()); } auto StateManager::FillGraph() -> void { @@ -62,7 +94,7 @@ auto StateManager::UpdateGraph() -> void { } visited_states.insert(current_state); - if (win_conditions[current_preset](current_state)) { + if (current_state.IsWon()) { winning_states.insert(current_state); } } @@ -85,20 +117,12 @@ auto StateManager::ClearGraph() -> void { auto StateManager::FindWinningStates() -> void { winning_states.clear(); for (const auto &[state, mass] : states) { - if (CurrentWinCondition()(state)) { + if (state.IsWon()) { winning_states.insert(state); } } } -auto StateManager::CurrentGenerator() const -> StateGenerator { - return generators[current_preset]; -} - -auto StateManager::CurrentWinCondition() const -> WinCondition { - return win_conditions[current_preset]; -} - auto StateManager::CurrentMassIndex() const -> std::size_t { return states.at(current_state); }