From 017d11245c7d851f96eac77bddd2abd1f34b4b04 Mon Sep 17 00:00:00 2001 From: Christoph Urlacher Date: Tue, 17 Feb 2026 13:26:52 +0100 Subject: [PATCH] begin implementing klotski logic --- CMakeLists.txt | 1 + include/klotski.hpp | 237 ++++++++++++++++++++++++++++++++++++++++++++ src/klotski.cpp | 130 ++++++++++++++++++++++++ 3 files changed, 368 insertions(+) create mode 100644 include/klotski.hpp create mode 100644 src/klotski.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index db948fe..c7b3c2e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -12,6 +12,7 @@ add_executable(masssprings src/main.cpp src/renderer.cpp src/mass_springs.cpp + src/klotski.cpp ) target_include_directories(masssprings PUBLIC ${RAYLIB_CPP_INCLUDE_DIR}) diff --git a/include/klotski.hpp b/include/klotski.hpp new file mode 100644 index 0000000..7a7a808 --- /dev/null +++ b/include/klotski.hpp @@ -0,0 +1,237 @@ +#ifndef __KLOTSKI_HPP_ +#define __KLOTSKI_HPP_ + +#include +#include +#include +#include +#include +#include + +// #define DBG_PRINT + +enum Direction { + NOR = 1 << 0, + EAS = 1 << 1, + SOU = 1 << 2, + WES = 1 << 3, +}; + +// A block is represented as a 2-digit string "wh", where w is the block width +// and h the block height. +// The target block (to remove from the board) is represented as a 2-letter +// string "xy", where x is the block width and y the block height and +// width/height are represented by [abcdefghi] (~= [123456789]). +class Block { +public: + int x; + int y; + int width; + int height; + bool target; + +public: + Block(int x, int y, int width, int height, bool target) + : x(x), y(y), width(width), height(height), target(target) { + if (x < 0 || x + width >= 10 || y < 0 || y + height >= 10) { + std::cerr << "Block must fit on a 9x9 board!" << std::endl; + exit(1); + } + +#ifdef DBG_PRINT + std::cout << ToString() << std::endl; +#endif + } + + Block(int x, int y, std::string block) : x(x), y(y) { + if (block == "..") { + this->x = 0; + this->y = 0; + width = 0; + height = 0; + target = false; + return; + } + + const std::array chars{'a', 'b', 'c', 'd', 'e', + 'f', 'g', 'h', 'i'}; + + target = false; + for (const char c : chars) { + if (block.contains(c)) { + target = true; + break; + } + } + + if (target) { + width = static_cast(block.at(0)) - static_cast('a') + 1; + height = static_cast(block.at(1)) - static_cast('a') + 1; + } else { + width = std::stoi(block.substr(0, 1)); + height = std::stoi(block.substr(1, 1)); + } + + if (x < 0 || x + width >= 10 || y < 0 || y + height >= 10) { + std::cerr << "Block must fit on a 9x9 board!" << std::endl; + exit(1); + } + if (block.length() != 2) { + std::cerr << "Block representation must have length [2]!" << std::endl; + exit(1); + } + +#ifdef DBG_PRINT + std::cout << "At: (" << x << ", " << y << "), Size: (" << width << ", " + << height << "), Target: " << target << std::endl; +#endif + } + + Block(const Block ©) + : x(copy.x), y(copy.y), width(copy.width), height(copy.height), + target(copy.target) {} + + Block &operator=(const Block ©) = delete; + + Block(Block &&move) + : x(move.x), y(move.y), width(move.width), height(move.height), + target(move.target) {} + + Block &operator=(Block &&move) = delete; + + bool operator==(const Block &other) { + return x == other.x && y == other.y && width && other.width && + target == other.target; + } + + bool operator!=(const Block &other) { return !(*this == other); } + + ~Block() {} + +public: + auto Hash() -> int; + + static auto Invalid() -> Block const; + + auto IsValid() -> bool; + + auto ToString() -> std::string; + + auto Covers(int xx, int yy) -> bool; + + auto Collides(const Block &other) -> 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. +class State { +public: + int width; + int height; + std::string state; + + // https://en.cppreference.com/w/cpp/iterator/input_iterator.html + class BlockIterator { + public: + using difference_type = std::ptrdiff_t; + using value_type = Block; + + private: + const State &state; + int current_pos; + + public: + BlockIterator(const State &state) : state(state), current_pos(0) {} + + BlockIterator(const State &state, int current_pos) + : state(state), current_pos(current_pos) {} + + Block operator*() const { + return Block(current_pos % state.width, current_pos / state.width, + state.state.substr(current_pos * 2 + 4, 2)); + } + + BlockIterator &operator++() { + do { + current_pos++; + } while (state.state.substr(current_pos * 2 + 4, 2) == ".."); + return *this; + } + + bool operator==(const BlockIterator &other) { + return state == other.state && current_pos == other.current_pos; + } + + bool operator!=(const BlockIterator &other) { return !(*this == other); } + }; + +public: + State(int width, int height) + : width(width), height(height), + state(std::format("{}x{}:{}", 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; + exit(1); + } + +#ifdef DBG_PRINT + std::cout << "State(" << width << ", " << height << "): \"" << state << "\"" + << std::endl; +#endif + } + + State(std::string state) + : width(std::stoi(state.substr(0, 1))), + height(std::stoi(state.substr(2, 1))), 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); + } + if (state.length() != width * height * 2 + 4) { + std::cerr + << "State representation must have length [width * height * 2 + 4]!" + << std::endl; + exit(1); + } + } + + State(const State ©) + : width(copy.width), height(copy.height), state(copy.state) {} + + State &operator=(const State ©) = delete; + + State(State &&move) + : width(move.width), height(move.height), state(std::move(move.state)) {} + + State &operator=(State &&move) = delete; + + bool operator==(const State &other) const { return state == other.state; } + + bool operator!=(const State &other) const { return !(*this == other); } + + BlockIterator begin() { return BlockIterator(*this); } + + BlockIterator end() { return BlockIterator(*this, width * height); } + + ~State() {} + +public: + auto Hash() -> int; + + auto AddBlock(Block block) -> bool; + + auto GetBlock(int x, int y) -> Block; + + auto RemoveBlock(int x, int y) -> bool; + + auto MoveBlockAt(int x, int y, Direction dir) -> bool; + + auto GetNextStates() -> std::vector; +}; + +#endif diff --git a/src/klotski.cpp b/src/klotski.cpp new file mode 100644 index 0000000..1c00f15 --- /dev/null +++ b/src/klotski.cpp @@ -0,0 +1,130 @@ +#include "klotski.hpp" + +auto Block::Hash() -> int { + std::string s = std::format("{},{},{},{}", x, y, width, height); + return std::hash{}(s); +} + +auto Block::Invalid() -> Block const { + 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::ToString() -> std::string { + if (target) { + return std::format("{}{}", + static_cast(width + static_cast('a') - 1), + static_cast(height + static_cast('a') - 1)); + } else { + return std::format("{}{}", width, height); + } +} + +auto Block::Covers(int xx, int yy) -> bool { + return xx >= x && xx < x + width && yy >= y && yy < y + height; +} + +auto Block::Collides(const Block &other) -> 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{}(state); } + +auto State::AddBlock(Block block) -> bool { + if (block.x + block.width > width || block.y + block.height > height) { + return false; + } + + for (Block b : *this) { + if (b.Collides(block)) { + return false; + } + } + + int index = 4 + (width * block.y + block.x) * 2; + state.replace(index, 2, block.ToString()); + + return true; +} + +auto State::GetBlock(int x, int y) -> Block { + if (x >= width || y >= height) { + return Block::Invalid(); + } + + for (Block b : *this) { + if (b.Covers(x, y)) { + return b; + } + } + + return Block::Invalid(); +} + +auto State::RemoveBlock(int x, int y) -> bool { + Block b = GetBlock(x, y); + if (!b.IsValid()) { + return false; + } + + int index = 4 + (width * b.y + b.x) * 2; + state.replace(index, 2, ".."); + + return true; +} + +auto State::MoveBlockAt(int x, int y, Direction dir) -> bool { + Block block = GetBlock(x, y); + if (!block.IsValid()) { + return false; + } + + // Get target block + int target_x = block.x; + int target_y = block.y; + switch (dir) { + case Direction::NOR: + if (target_y < 1) { + return false; + } + target_y--; + break; + case Direction::EAS: + if (target_x + block.width >= width) { + return false; + } + target_x++; + break; + case Direction::SOU: + if (target_y + block.height >= height) { + return false; + } + target_y++; + break; + case Direction::WES: + if (target_x < 1) { + return false; + } + target_x--; + break; + } + Block target = + Block(target_x, target_y, block.width, block.height, block.target); + + // Check collisions + for (Block b : *this) { + if (b != block && b.Collides(target)) { + return false; + } + } + + RemoveBlock(x, y); + AddBlock(target); + + return true; +}