diff --git a/.gitignore b/.gitignore index c218d34..05237a4 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ bin/ lib/ # IDE and editor files -.vscode/ .idea/ *.swp *.swo diff --git a/.gitmodules b/.gitmodules index 1e4823a..bf0badc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,3 @@ -[submodule "thirdparty/SDL"] - path = thirdparty/SDL - url = https://github.com/libsdl-org/SDL.git [submodule "thirdparty/rapidjson"] path = thirdparty/rapidjson url = https://github.com/pah/rapidjson.git -[submodule "thirdparty/assimp"] - path = thirdparty/assimp - url = https://github.com/assimp/assimp.git -[submodule "thirdparty/SDL_image"] - path = thirdparty/SDL_image - url = https://github.com/libsdl-org/SDL_image.git diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..715554e --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + "name": "Debug Sudoku", + "type": "", + "request": "launch", + "program": "${workspaceFolder}/demos/sudoku/build/sudoku", + "args": [], + "stopAtEntry": false, + "cwd": "${workspaceFolder}/demos/sudoku", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ], + "preLaunchTask": "Build Sudoku" + ] + } \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..8673ba3 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,21 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Build Sudoku", + "type": "shell", + "command": "cmake --build ${workspaceFolder}/demos/sudoku/build --config Debug", + "group": "build", + "problemMatcher": [], + "detail": "Build the sudoku demo" + }, + { + "label": "Run Sudoku", + "type": "shell", + "command": "${workspaceFolder}/demos/sudoku/build/bin/sudoku_wfc_demo", + "group": "test", + "problemMatcher": [], + "detail": "Run the sudoku demo" + }, + ] + } \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index f932dc3..d65c07a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.16) project(nd-wfc VERSION 0.1.0 LANGUAGES CXX) # Set C++ standard -set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -15,7 +15,6 @@ option(ND_WFC_BUILD_EXAMPLES "Build examples" ON) option(ND_WFC_USE_SYSTEM_LIBS "Use system libraries instead of bundled" OFF) # Add subdirectories -add_subdirectory(thirdparty) add_subdirectory(src) if(ND_WFC_BUILD_TESTS) diff --git a/README.md b/README.md index e84893d..3947499 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,33 @@ -# N-Dimensional Wave Function Collapse (nd-wfc) +# N-Dimensional Wave Function Collapse (WFC) Library -An optimized C++ implementation of the Wave Function Collapse algorithm that supports multiple dimensions (2D, 3D, 4D, etc.). +A templated C++20 Wave Function Collapse engine that can work with 2D grids, 3D grids, Sudoku, Picross, and any other constraint satisfaction problem. The engine is coordinate-system agnostic and can work with any graph-like structure. ## Features -- **N-dimensional support**: Works with 2D, 3D, 4D, and higher dimensions -- **Optimized performance**: Efficient constraint propagation and entropy calculation -- **Flexible tile system**: Support for weighted tiles and custom constraints -- **Third-party integration**: Built-in support for JSON (RapidJSON), textures (STB), and 3D models (Assimp) -- **SDL2 graphics**: Windowing and rendering capabilities for visualization -- **Comprehensive testing**: Google Test integration for robust testing +- **Templated Design**: Works with any World type that satisfies the `WorldConcept` +- **Builder Pattern**: Easy to use fluent interface for setting up WFC systems +- **Constraint System**: Flexible constraint definition using lambda functions +- **Multiple World Types**: Built-in support for 2D/3D arrays, graphs, and Sudoku grids +- **Coordinate Agnostic**: Works without coordinate systems using only cell IDs +- **C++20**: Uses modern C++ features like concepts, ranges, and smart pointers ## Project Structure ``` nd-wfc/ ├── CMakeLists.txt # Main CMake configuration -├── cmake/ -│ └── nd-wfc-config.cmake.in # CMake package config ├── include/nd-wfc/ # Public headers -│ ├── types.hpp # Core types and enums -│ ├── wfc.hpp # Main WFC class -│ ├── grid.hpp # N-dimensional grid -│ ├── wave.hpp # Wave function representation -│ ├── propagator.hpp # Constraint propagator -│ ├── entropy.hpp # Entropy calculator -│ ├── constraint.hpp # Constraint management -│ └── tile.hpp # Tile system +│ ├── wfc.h # Main include file +│ ├── wfc.hpp # Core WFC classes and builder pattern +│ └── worlds.hpp # Built-in World type implementations ├── src/ # Implementation files -├── tests/ # Google Test files │ ├── CMakeLists.txt -│ ├── test_main.cpp -│ └── test_wfc.cpp +│ └── main.cpp # Basic test program ├── examples/ # Example programs │ ├── CMakeLists.txt -│ ├── basic_wfc_2d.cpp -│ ├── basic_wfc_3d.cpp -│ ├── texture_generation.cpp -│ └── model_generation.cpp -├── thirdparty/ # External dependencies -│ └── CMakeLists.txt +│ ├── tilemap_example.cpp # 2D tilemap generation +│ ├── sudoku_example.cpp # Full 9x9 Sudoku solver +│ └── simple_sudoku_example.cpp # Simple 2x2 Sudoku for testing └── build/ # Build directory (generated) ``` @@ -48,7 +36,7 @@ nd-wfc/ ### Prerequisites - CMake 3.16 or higher -- C++17 compatible compiler (GCC 8+, Clang 8+, MSVC 2019+) +- C++20 compatible compiler (GCC 10+, Clang 10+, MSVC 2019+) - Git ### Build Instructions @@ -66,78 +54,186 @@ cd build cmake .. # Build the project -cmake --build . --config Release +make -# Run tests -ctest --output-on-failure +# Run basic test +./src/nd-wfc-main # Run examples -./examples/basic_wfc_2d +./examples/tilemap_example +./examples/simple_sudoku_example ``` -### CMake Options - -- `ND_WFC_BUILD_TESTS=ON/OFF` - Build tests (default: ON) -- `ND_WFC_BUILD_EXAMPLES=ON/OFF` - Build examples (default: ON) -- `ND_WFC_USE_SYSTEM_LIBS=ON/OFF` - Use system libraries instead of bundled (default: OFF) - ## Usage -### Basic 2D Example +### Basic Example (2x2 Grid) ```cpp -#include "nd-wfc/wfc.hpp" -#include "nd-wfc/types.hpp" +#include +#include int main() { - // Create a 10x10 grid - nd_wfc::Size<2> size = {10, 10}; + enum SimpleTiles { A = 1, B = 2 }; - // Define tile types - std::vector tiles = {0, 1, 2, 3}; + auto wfc = WFC::Builder>() + .variable(A, [](WFC::Array2D& world, int worldID, + WFC::Constrainer& constrainer, uint8_t var) { + auto [x, y] = world.getCoord(worldID); + if (y > 0) constrainer.only((y-1) * 2 + x, B); + if (y < 1) constrainer.only((y+1) * 2 + x, B); + if (x > 0) constrainer.only(y * 2 + (x-1), B); + if (x < 1) constrainer.only(y * 2 + (x+1), B); + }) + .variable(B, [](WFC::Array2D& world, int worldID, + WFC::Constrainer& constrainer, uint8_t var) { + auto [x, y] = world.getCoord(worldID); + if (y > 0) constrainer.only((y-1) * 2 + x, A); + if (y < 1) constrainer.only((y+1) * 2 + x, A); + if (x > 0) constrainer.only(y * 2 + (x-1), A); + if (x < 1) constrainer.only(y * 2 + (x+1), A); + }) + .build(WFC::Array2D()); - // Define constraints (adjacency rules) - std::vector constraints = { - {0, 0, nd_wfc::Direction::North}, // Tile 0 can connect to tile 0 from North - {0, 1, nd_wfc::Direction::South}, // Tile 0 can connect to tile 1 from South - // ... more constraints - }; - - // Configure WFC - nd_wfc::WfcConfig config; - config.seed = 12345; - - // Create and run WFC - nd_wfc::Wfc2D wfc(size, tiles, constraints, config); - nd_wfc::WfcResult result = wfc.run(); - - if (result == nd_wfc::WfcResult::Success) { - // Access the result - const auto& grid = wfc.getGrid(); - // Use grid data... + if (wfc->run()) { + std::cout << "Solution found!" << std::endl; + // Print solution... } return 0; } ``` -### 3D Example +### 2D Tilemap Example ```cpp -// Create a 3D WFC instance -nd_wfc::Size<3> size = {8, 8, 8}; -nd_wfc::Wfc3D wfc(size, tiles, constraints, config); +enum TileType { SEA = 1, BEACH = 2, LAND = 3 }; + +auto wfc = WFC::Builder>() + .variable(SEA, [](WFC::Array2D& world, int worldID, + WFC::Constrainer& constrainer, uint8_t var) { + auto [x, y] = world.getCoord(worldID); + // Adjacent cells should be SEA or BEACH (no direct LAND next to SEA) + if (y > 0) constrainer.only((y-1) * 100 + x, SEA, BEACH); + if (y < 99) constrainer.only((y+1) * 100 + x, SEA, BEACH); + if (x > 0) constrainer.only(y * 100 + (x-1), SEA, BEACH); + if (x < 99) constrainer.only(y * 100 + (x+1), SEA, BEACH); + }) + .variable(BEACH, [](WFC::Array2D& world, int worldID, + WFC::Constrainer& constrainer, uint8_t var) { + auto [x, y] = world.getCoord(worldID); + // Adjacent cells should be SEA or LAND (BEACH is transition between them) + if (y > 0) constrainer.only((y-1) * 100 + x, SEA, LAND); + if (y < 99) constrainer.only((y+1) * 100 + x, SEA, LAND); + if (x > 0) constrainer.only(y * 100 + (x-1), SEA, LAND); + if (x < 99) constrainer.only(y * 100 + (x+1), SEA, LAND); + }) + .variable(LAND, [](WFC::Array2D& world, int worldID, + WFC::Constrainer& constrainer, uint8_t var) { + auto [x, y] = world.getCoord(worldID); + // Adjacent cells should be LAND or BEACH (no direct SEA next to LAND) + if (y > 0) constrainer.only((y-1) * 100 + x, LAND, BEACH); + if (y < 99) constrainer.only((y+1) * 100 + x, LAND, BEACH); + if (x > 0) constrainer.only(y * 100 + (x-1), LAND, BEACH); + if (x < 99) constrainer.only(y * 100 + (x+1), LAND, BEACH); + }) + .build(WFC::Array2D()); ``` -## Third-party Libraries +## World Types -The project includes several third-party libraries: +The library provides several built-in World types: -- **RapidJSON**: JSON parsing and serialization -- **STB**: Image loading and manipulation (STB Image, STB Image Write) -- **Assimp**: 3D model loading and processing -- **SDL2**: Graphics and windowing -- **Google Test**: Unit testing framework +### Array2D +2D array with compile-time dimensions. + +```cpp +WFC::Array2D world; +``` + +### Array3D +3D array with compile-time dimensions. + +```cpp +WFC::Array3D world; +``` + +### GraphWorld +Graph-based world for non-grid structures. + +```cpp +WFC::GraphWorld world(100); // 100 nodes +world.addEdge(0, 1); // Connect nodes +``` + +### SudokuWorld +Specialized 9x9 grid with Sudoku helper functions. + +```cpp +WFC::SudokuWorld world; +auto rowCells = world.getRowCells(cellId); +auto colCells = world.getColumnCells(cellId); +auto boxCells = world.getBoxCells(cellId); +``` + +## Custom World Types + +To create a custom World type, implement these requirements: + +```cpp +struct MyWorld { + using ValueType = MyValueType; + using CoordType = MyCoordType; + + size_t size() const; + int getId(CoordType coord) const; + CoordType getCoord(int id) const; +}; +``` + +## API Reference + +### Builder + +- `Builder& variable(VarT value, ConstraintFunc func)` - Add a variable with constraint function +- `std::unique_ptr> build(WorldT world)` - Build the WFC instance + +### WFC + +- `bool run()` - Run the WFC algorithm +- `std::optional getValue(int cellId)` - Get value at cell (if collapsed) +- `const std::unordered_set& getPossibleValues(int cellId)` - Get possible values for cell + +### Constrainer + +- `void constrain(int cellId, const std::unordered_set& allowedValues)` - Constrain to specific values +- `void only(int cellId, int value)` - Constrain to single value +- `void only(int cellId, int value1, int value2)` - Constrain to two values + +## Examples + +The `examples/` directory contains: + +- `tilemap_example.cpp` - 2D tilemap generation with Sea/Beach/Land +- `sudoku_example.cpp` - Full 9x9 Sudoku solver +- `simple_sudoku_example.cpp` - Simple 2x2 Sudoku for testing + +## Running Examples + +```bash +cd build/examples +./tilemap_example # Generate a 100x100 tilemap +./simple_sudoku_example # Solve a simple 2x2 Sudoku +``` + +## Architecture + +The WFC engine consists of: + +1. **World Types**: Define the problem space and coordinate systems +2. **Builder Pattern**: Fluent interface for setting up variables and constraints +3. **Constraint System**: Lambda-based constraint definitions +4. **Wave Function Collapse Algorithm**: Core WFC implementation with observation and propagation +5. **Propagation Queue**: Efficient constraint propagation system ## Contributing @@ -155,5 +251,4 @@ This project is licensed under the MIT License - see the LICENSE file for detail ## References - Original WFC Algorithm: https://github.com/mxgmn/WaveFunctionCollapse -- N-dimensional WFC concepts and optimizations -- Various academic papers on constraint satisfaction and procedural generation +- Constraint satisfaction and procedural generation concepts diff --git a/demos/sudoku/CMakeLists.txt b/demos/sudoku/CMakeLists.txt index 0ee75ec..11f07e3 100644 --- a/demos/sudoku/CMakeLists.txt +++ b/demos/sudoku/CMakeLists.txt @@ -39,20 +39,38 @@ add_executable(sudoku_demo sudoku.cpp ) -# Set output directory +# Create WFC demo executable +add_executable(sudoku_wfc_demo + sudoku_wfc.cpp +) + +# Set output directory for sudoku_demo set_target_properties(sudoku_demo PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin ) +# Set output directory and properties for sudoku_wfc_demo +set_target_properties(sudoku_wfc_demo PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON +) + # Include directories target_include_directories(sudoku_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +target_include_directories(sudoku_wfc_demo PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../include +) # Optional: Enable optimizations for release builds if(CMAKE_BUILD_TYPE STREQUAL "Release") if(MSVC) target_compile_options(sudoku_demo PRIVATE /O2) + target_compile_options(sudoku_wfc_demo PRIVATE /O2) else() target_compile_options(sudoku_demo PRIVATE -O3 -march=native) + target_compile_options(sudoku_wfc_demo PRIVATE -O3 -march=native) endif() endif() @@ -97,9 +115,9 @@ endif() # Installation (optional) if(HAS_GTEST AND HAS_BENCHMARK) - install(TARGETS sudoku_demo sudoku_tests sudoku_benchmarks DESTINATION bin) + install(TARGETS sudoku_demo sudoku_wfc_demo sudoku_tests sudoku_benchmarks DESTINATION bin) elseif(HAS_GTEST) - install(TARGETS sudoku_demo sudoku_tests DESTINATION bin) + install(TARGETS sudoku_demo sudoku_wfc_demo sudoku_tests DESTINATION bin) else() - install(TARGETS sudoku_demo DESTINATION bin) + install(TARGETS sudoku_demo sudoku_wfc_demo DESTINATION bin) endif() diff --git a/demos/sudoku/main.cpp b/demos/sudoku/main.cpp new file mode 100644 index 0000000..ae5c34f --- /dev/null +++ b/demos/sudoku/main.cpp @@ -0,0 +1,19 @@ +#include "sudoku.h" +#include + +int main() +{ + std::cout << "Sudoku Demo" << std::endl; + + // Create a simple sudoku puzzle + Sudoku sudoku("530070000600195000098000060800060003400803001700020006060000280000419005000080079"); + + if (sudoku.isValid()) { + std::cout << "Loaded valid sudoku puzzle:" << std::endl; + sudoku.print(); + } else { + std::cout << "Invalid sudoku puzzle!" << std::endl; + } + + return 0; +} diff --git a/demos/sudoku/sudoku_wfc.cpp b/demos/sudoku/sudoku_wfc.cpp new file mode 100644 index 0000000..81c85dd --- /dev/null +++ b/demos/sudoku/sudoku_wfc.cpp @@ -0,0 +1,58 @@ +#include +#include +#include + +int main() +{ + std::cout << "Running Sudoku WFC" << std::endl; + + auto sudokuSolver = WFC::Builder, uint8_t>() + .DefineIDs<1, 2, 3, 4, 5, 6, 7, 8, 9>() + .Variable<1, 2, 3, 4, 5, 6, 7, 8, 9>([](WFC::Array2D&, size_t index, WFC::WorldValue val, auto& constrainer) { + size_t x = index % 9; + size_t y = index / 9; + + // Add row constraints (same row, different columns) + for (size_t i = 0; i < 9; ++i) { + if (i != x) constrainer.Exclude(val, i + y * 9); + } + + // Add column constraints (same column, different rows) + for (size_t i = 0; i < 9; ++i) { + if (i != y) constrainer.Exclude(val,x + i * 9); + } + + // Add box constraints (3x3 box) + int box_x = (x / 3) * 3; + int box_y = (y / 3) * 3; + for (size_t j = 0; j < 3; ++j) { + for (size_t k = 0; k < 3; ++k) { + int cell_x = box_x + j; + int cell_y = box_y + k; + size_t cell_index = cell_x + cell_y * 9; + if (cell_index != index) { + constrainer.Exclude(val, cell_index); + } + } + } + }) + .build(); + + WFC::Array2D sudokuWorld; + bool success = sudokuSolver.Run(sudokuWorld); + + if (success) { + std::cout << "Sudoku solved successfully!" << std::endl; + // Print the solved sudoku + for (size_t y = 0; y < 9; ++y) { + for (size_t x = 0; x < 9; ++x) { + std::cout << static_cast(sudokuWorld.at(x, y)) << " "; + if (x == 2 || x == 5) std::cout << "| "; + } + std::cout << std::endl; + if (y == 2 || y == 5) std::cout << "------+-------+------" << std::endl; + } + } else { + std::cout << "Failed to solve sudoku!" << std::endl; + } +} \ No newline at end of file diff --git a/demos/sudoku/sudoku_wfc_demo b/demos/sudoku/sudoku_wfc_demo new file mode 100755 index 0000000..090a8d0 Binary files /dev/null and b/demos/sudoku/sudoku_wfc_demo differ diff --git a/demos/sudoku/sudoku_wfc_manual b/demos/sudoku/sudoku_wfc_manual new file mode 100755 index 0000000..090a8d0 Binary files /dev/null and b/demos/sudoku/sudoku_wfc_manual differ diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 9254f3b..293a474 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,5 +1,5 @@ set(EXAMPLE_SOURCES - basic_wfc_2d.cpp + ) # Create executables for each example @@ -29,6 +29,5 @@ if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/resources) ${CMAKE_CURRENT_SOURCE_DIR}/resources ${CMAKE_BINARY_DIR}/examples/resources ) - add_dependencies(basic_wfc_2d copy-example-resources) - add_dependencies(texture_generation copy-example-resources) + # Dependencies for resource copying would go here if needed endif() diff --git a/examples/basic_wfc_2d.cpp b/examples/basic_wfc_2d.cpp deleted file mode 100644 index 1b77e6c..0000000 --- a/examples/basic_wfc_2d.cpp +++ /dev/null @@ -1,88 +0,0 @@ -#include -#include -#include "nd-wfc/wfc.hpp" -#include "nd-wfc/types.hpp" - -int main() { - std::cout << "Running 2D WFC Example" << std::endl; - - // Create a 10x10 grid - nd_wfc::Size<2> size = {10, 10}; - - // Define 4 tile types (like a simple terrain system) - std::vector tiles = {0, 1, 2, 3}; // Empty, Grass, Water, Mountain - - // Define adjacency constraints - std::vector constraints = { - // Empty can connect to anything - {0, 0, nd_wfc::Direction::North}, {0, 0, nd_wfc::Direction::South}, - {0, 0, nd_wfc::Direction::East}, {0, 0, nd_wfc::Direction::West}, - {0, 1, nd_wfc::Direction::North}, {0, 1, nd_wfc::Direction::South}, - {0, 1, nd_wfc::Direction::East}, {0, 1, nd_wfc::Direction::West}, - {0, 2, nd_wfc::Direction::North}, {0, 2, nd_wfc::Direction::South}, - {0, 2, nd_wfc::Direction::East}, {0, 2, nd_wfc::Direction::West}, - {0, 3, nd_wfc::Direction::North}, {0, 3, nd_wfc::Direction::South}, - {0, 3, nd_wfc::Direction::East}, {0, 3, nd_wfc::Direction::West}, - - // Grass can connect to Empty, Grass, and Mountain - {1, 0, nd_wfc::Direction::North}, {1, 0, nd_wfc::Direction::South}, - {1, 0, nd_wfc::Direction::East}, {1, 0, nd_wfc::Direction::West}, - {1, 1, nd_wfc::Direction::North}, {1, 1, nd_wfc::Direction::South}, - {1, 1, nd_wfc::Direction::East}, {1, 1, nd_wfc::Direction::West}, - {1, 3, nd_wfc::Direction::North}, {1, 3, nd_wfc::Direction::South}, - {1, 3, nd_wfc::Direction::East}, {1, 3, nd_wfc::Direction::West}, - - // Water can only connect to Empty and Water - {2, 0, nd_wfc::Direction::North}, {2, 0, nd_wfc::Direction::South}, - {2, 0, nd_wfc::Direction::East}, {2, 0, nd_wfc::Direction::West}, - {2, 2, nd_wfc::Direction::North}, {2, 2, nd_wfc::Direction::South}, - {2, 2, nd_wfc::Direction::East}, {2, 2, nd_wfc::Direction::West}, - - // Mountain can connect to Empty, Grass, and Mountain - {3, 0, nd_wfc::Direction::North}, {3, 0, nd_wfc::Direction::South}, - {3, 0, nd_wfc::Direction::East}, {3, 0, nd_wfc::Direction::West}, - {3, 1, nd_wfc::Direction::North}, {3, 1, nd_wfc::Direction::South}, - {3, 1, nd_wfc::Direction::East}, {3, 1, nd_wfc::Direction::West}, - {3, 3, nd_wfc::Direction::North}, {3, 3, nd_wfc::Direction::South}, - {3, 3, nd_wfc::Direction::East}, {3, 3, nd_wfc::Direction::West}, - }; - - // Configure the WFC algorithm - nd_wfc::WfcConfig config; - config.seed = 12345; // For reproducible results - config.max_iterations = 10000; - - // Create and run WFC - nd_wfc::Wfc2D wfc(size, tiles, constraints, config); - - std::cout << "Running WFC algorithm..." << std::endl; - nd_wfc::WfcResult result = wfc.run(); - - if (result == nd_wfc::WfcResult::Success) { - std::cout << "WFC completed successfully!" << std::endl; - std::cout << "Iterations: " << wfc.getIterationCount() << std::endl; - - // Print the result (simple ASCII representation) - const auto& grid = wfc.getGrid(); - for (nd_wfc::Index y = 0; y < size[1]; ++y) { - for (nd_wfc::Index x = 0; x < size[0]; ++x) { - nd_wfc::Coord<2> coord = {x, y}; - nd_wfc::TileId tile = grid[coord]; - char symbol; - switch (tile) { - case 0: symbol = '.'; break; // Empty - case 1: symbol = 'G'; break; // Grass - case 2: symbol = 'W'; break; // Water - case 3: symbol = 'M'; break; // Mountain - default: symbol = '?'; break; - } - std::cout << symbol << ' '; - } - std::cout << std::endl; - } - } else { - std::cout << "WFC failed with result: " << static_cast(result) << std::endl; - } - - return 0; -} diff --git a/include/nd-wfc/constraint.hpp b/include/nd-wfc/constraint.hpp deleted file mode 100644 index 6fe74ec..0000000 --- a/include/nd-wfc/constraint.hpp +++ /dev/null @@ -1,125 +0,0 @@ -#pragma once - -#include "types.hpp" -#include -#include -#include - -namespace nd_wfc { - -// Constraint manager for efficient adjacency rule lookups -class ConstraintManager { -private: - // Map from (tile, direction) to set of allowed adjacent tiles - std::unordered_map> adjacency_rules_; - std::vector tiles_; - std::unordered_map tile_to_index_; - -public: - ConstraintManager(const std::vector& tiles, const std::vector& constraints) - : tiles_(tiles) { - - // Build tile index mapping - for (Index i = 0; i < tiles_.size(); ++i) { - tile_to_index_[tiles_[i]] = i; - } - - // Build adjacency rules from constraints - for (const auto& constraint : constraints) { - std::uint64_t key = makeKey(constraint.from_tile, constraint.direction); - adjacency_rules_[key].insert(constraint.to_tile); - } - } - - // Check if two tiles can be adjacent in a specific direction - bool isAllowed(TileId from_tile, TileId to_tile, Direction direction) const { - std::uint64_t key = makeKey(from_tile, direction); - auto it = adjacency_rules_.find(key); - if (it == adjacency_rules_.end()) { - return false; // No rules defined, assume not allowed - } - return it->second.count(to_tile) > 0; - } - - // Get all allowed adjacent tiles for a given tile and direction - std::vector getAllowedTiles(TileId from_tile, Direction direction) const { - std::uint64_t key = makeKey(from_tile, direction); - auto it = adjacency_rules_.find(key); - if (it == adjacency_rules_.end()) { - return {}; // No rules defined - } - - std::vector result; - result.reserve(it->second.size()); - for (TileId tile : it->second) { - result.push_back(tile); - } - return result; - } - - // Get all allowed adjacent tile indices (for bitset operations) - TileMask getAllowedTileMask(TileId from_tile, Direction direction) const { - TileMask mask; - std::uint64_t key = makeKey(from_tile, direction); - auto it = adjacency_rules_.find(key); - if (it != adjacency_rules_.end()) { - for (TileId tile : it->second) { - auto tile_it = tile_to_index_.find(tile); - if (tile_it != tile_to_index_.end()) { - mask.set(tile_it->second); - } - } - } - return mask; - } - - // Get tile index from tile ID - Index getTileIndex(TileId tile_id) const { - auto it = tile_to_index_.find(tile_id); - return (it != tile_to_index_.end()) ? it->second : static_cast(-1); - } - - // Get tile ID from index - TileId getTileId(Index index) const { - assert(index < tiles_.size()); - return tiles_[index]; - } - - // Get all tiles - const std::vector& getTiles() const { return tiles_; } - Index getNumTiles() const { return tiles_.size(); } - - // Validate constraints (basic validation for now) - bool validateConstraints() const { - // For now, just check that we have some constraints - // In a more sophisticated implementation, we could check for symmetry - // but for performance reasons, we'll assume the user provides correct constraints - return !adjacency_rules_.empty(); - } - - // Add a constraint - void addConstraint(const Constraint& constraint) { - std::uint64_t key = makeKey(constraint.from_tile, constraint.direction); - adjacency_rules_[key].insert(constraint.to_tile); - } - - // Add symmetric constraint (automatically adds the reverse) - void addSymmetricConstraint(TileId tile1, TileId tile2, Direction direction) { - addConstraint({tile1, tile2, direction}); - addConstraint({tile2, tile1, getOppositeDirection(direction)}); - } - -private: - // Create a unique key for (tile, direction) pairs - std::uint64_t makeKey(TileId tile, Direction direction) const { - return (static_cast(tile) << 8) | static_cast(direction); - } - - // Unpack a key back to (tile, direction) - void unpackKey(std::uint64_t key, TileId& tile, Direction& direction) const { - tile = static_cast(key >> 8); - direction = static_cast(key & 0xFF); - } -}; - -} // namespace nd_wfc diff --git a/include/nd-wfc/corefiles-here b/include/nd-wfc/corefiles-here deleted file mode 100644 index e69de29..0000000 diff --git a/include/nd-wfc/entropy.hpp b/include/nd-wfc/entropy.hpp deleted file mode 100644 index acfcaf5..0000000 --- a/include/nd-wfc/entropy.hpp +++ /dev/null @@ -1,174 +0,0 @@ -#pragma once - -#include "types.hpp" -#include "wave.hpp" -#include -#include -#include - -namespace nd_wfc { - -// Entropy calculator for efficient cell selection during WFC -template -class EntropyCalculator { -private: - const Wave* wave_; - mutable std::vector entropy_sorted_indices_; - mutable bool cache_dirty_; - -public: - explicit EntropyCalculator(const Wave* wave) - : wave_(wave), cache_dirty_(true) { - entropy_sorted_indices_.reserve(wave_->getTotalSize()); - } - - // Find the cell with minimum entropy (heuristic for next cell to collapse) - bool findMinEntropyCell(Coord& coord, std::mt19937& rng) const { - updateCache(); - - if (entropy_sorted_indices_.empty()) { - return false; // All cells collapsed or contradiction - } - - // Get the minimum entropy value - Index first_index = entropy_sorted_indices_[0]; - Coord first_coord = wave_->indexToCoord(first_index); - std::uint8_t min_entropy = wave_->getEntropy(first_coord); - - if (min_entropy == 0) { - return false; // Contradiction - } - - // Find all cells with the same minimum entropy - std::vector candidates; - for (Index idx : entropy_sorted_indices_) { - Coord test_coord = wave_->indexToCoord(idx); - if (wave_->getEntropy(test_coord) == min_entropy) { - candidates.push_back(idx); - } else { - break; // Since sorted, no more minimum entropy cells - } - } - - // Randomly select among candidates - std::uniform_int_distribution dist(0, candidates.size() - 1); - Index chosen_index = candidates[dist(rng)]; - coord = wave_->indexToCoord(chosen_index); - - return true; - } - - // Find all cells with minimum entropy - std::vector> findMinEntropyCells() const { - updateCache(); - - if (entropy_sorted_indices_.empty()) { - return {}; - } - - // Get the minimum entropy value - Index first_index = entropy_sorted_indices_[0]; - Coord first_coord = wave_->indexToCoord(first_index); - std::uint8_t min_entropy = wave_->getEntropy(first_coord); - - if (min_entropy == 0) { - return {}; // Contradiction - } - - std::vector> result; - for (Index idx : entropy_sorted_indices_) { - Coord coord = wave_->indexToCoord(idx); - if (wave_->getEntropy(coord) == min_entropy) { - result.push_back(coord); - } else { - break; // Since sorted, no more minimum entropy cells - } - } - - return result; - } - - // Get entropy statistics - struct EntropyStats { - std::uint8_t min_entropy; - std::uint8_t max_entropy; - float average_entropy; - Index uncollapsed_cells; - }; - - EntropyStats getStats() const { - updateCache(); - - if (entropy_sorted_indices_.empty()) { - return {0, 0, 0.0f, 0}; - } - - std::uint8_t min_entropy = 255; - std::uint8_t max_entropy = 0; - std::uint32_t total_entropy = 0; - Index uncollapsed_count = entropy_sorted_indices_.size(); - - for (Index idx : entropy_sorted_indices_) { - Coord coord = wave_->indexToCoord(idx); - std::uint8_t entropy = wave_->getEntropy(coord); - - min_entropy = std::min(min_entropy, entropy); - max_entropy = std::max(max_entropy, entropy); - total_entropy += entropy; - } - - float avg_entropy = uncollapsed_count > 0 ? - static_cast(total_entropy) / uncollapsed_count : 0.0f; - - return {min_entropy, max_entropy, avg_entropy, uncollapsed_count}; - } - - // Mark cache as dirty (call when wave function changes) - void markDirty() { - cache_dirty_ = true; - } - - // Check if there are any uncollapsed cells - bool hasUncollapedCells() const { - updateCache(); - return !entropy_sorted_indices_.empty(); - } - -private: - // Update the internal cache of sorted indices - void updateCache() const { - if (!cache_dirty_) { - return; - } - - entropy_sorted_indices_.clear(); - - // Collect all uncollapsed cell indices - for (Index i = 0; i < wave_->getTotalSize(); ++i) { - Coord coord = wave_->indexToCoord(i); - if (!wave_->isCollapsed(coord)) { - entropy_sorted_indices_.push_back(i); - } - } - - // Sort by entropy (ascending order) - std::sort(entropy_sorted_indices_.begin(), entropy_sorted_indices_.end(), - [this](Index a, Index b) { - Coord coord_a = wave_->indexToCoord(a); - Coord coord_b = wave_->indexToCoord(b); - std::uint8_t entropy_a = wave_->getEntropy(coord_a); - std::uint8_t entropy_b = wave_->getEntropy(coord_b); - - if (entropy_a != entropy_b) { - return entropy_a < entropy_b; - } - - // For cells with same entropy, use index as tiebreaker for determinism - return a < b; - }); - - cache_dirty_ = false; - } -}; - -} // namespace nd_wfc diff --git a/include/nd-wfc/grid.hpp b/include/nd-wfc/grid.hpp deleted file mode 100644 index a328f42..0000000 --- a/include/nd-wfc/grid.hpp +++ /dev/null @@ -1,146 +0,0 @@ -#pragma once - -#include "types.hpp" -#include -#include - -namespace nd_wfc { - -// N-dimensional grid for storing tiles and managing coordinate transformations -template -class Grid { -private: - Size size_; - std::vector data_; - Index total_size_; - std::array strides_; - -public: - explicit Grid(const Size& size) : size_(size) { - // Calculate total size and strides for efficient coordinate mapping - total_size_ = 1; - for (std::size_t i = 0; i < N; ++i) { - total_size_ *= size_[i]; - } - - // Calculate strides for row-major ordering - strides_[N - 1] = 1; - for (std::size_t i = N - 1; i > 0; --i) { - strides_[i - 1] = strides_[i] * size_[i]; - } - - // Initialize with invalid tile ID - data_.resize(total_size_, static_cast(-1)); - } - - // Convert n-dimensional coordinate to linear index - Index coordToIndex(const Coord& coord) const { - Index index = 0; - for (std::size_t i = 0; i < N; ++i) { - assert(coord[i] < size_[i]); - index += coord[i] * strides_[i]; - } - return index; - } - - // Convert linear index to n-dimensional coordinate - Coord indexToCoord(Index index) const { - assert(index < total_size_); - Coord coord; - for (std::size_t i = 0; i < N; ++i) { - coord[i] = (index / strides_[i]) % size_[i]; - } - return coord; - } - - // Check if coordinate is within bounds - bool isValidCoord(const Coord& coord) const { - for (std::size_t i = 0; i < N; ++i) { - if (coord[i] >= size_[i]) { - return false; - } - } - return true; - } - - // Get neighbor coordinate in a specific direction - bool getNeighbor(const Coord& coord, Direction dir, Coord& neighbor) const { - auto offset = getDirectionOffset(dir); - - for (std::size_t i = 0; i < N; ++i) { - // Handle signed arithmetic carefully to avoid underflow - if (offset[i] < 0) { - if (coord[i] < static_cast(-offset[i])) { - return false; // Would underflow - } - neighbor[i] = coord[i] - static_cast(-offset[i]); - } else { - neighbor[i] = coord[i] + static_cast(offset[i]); - if (neighbor[i] >= size_[i]) { - return false; // Out of bounds - } - } - } - return true; - } - - // Access tile at coordinate - TileId& operator[](const Coord& coord) { - return data_[coordToIndex(coord)]; - } - - const TileId& operator[](const Coord& coord) const { - return data_[coordToIndex(coord)]; - } - - // Access tile by linear index - TileId& operator[](Index index) { - assert(index < total_size_); - return data_[index]; - } - - const TileId& operator[](Index index) const { - assert(index < total_size_); - return data_[index]; - } - - // Getters - const Size& getSize() const { return size_; } - Index getTotalSize() const { return total_size_; } - - // Get all neighbor coordinates for a given position - std::vector, Direction>> getNeighbors(const Coord& coord) const { - std::vector, Direction>> neighbors; - auto valid_dirs = getValidDirections(); - - for (Direction dir : valid_dirs) { - Coord neighbor; - if (getNeighbor(coord, dir, neighbor)) { - neighbors.emplace_back(neighbor, dir); - } - } - return neighbors; - } - - // Clear the grid - void clear() { - std::fill(data_.begin(), data_.end(), static_cast(-1)); - } - - // Check if all cells are filled - bool isComplete() const { - for (const auto& tile : data_) { - if (tile == static_cast(-1)) { - return false; - } - } - return true; - } -}; - -// Type aliases for common dimensions -using Grid2D = Grid<2>; -using Grid3D = Grid<3>; -using Grid4D = Grid<4>; - -} // namespace nd_wfc diff --git a/include/nd-wfc/propagator.hpp b/include/nd-wfc/propagator.hpp deleted file mode 100644 index 8df1ecc..0000000 --- a/include/nd-wfc/propagator.hpp +++ /dev/null @@ -1,185 +0,0 @@ -#pragma once - -#include "types.hpp" -#include "wave.hpp" -#include "constraint.hpp" -#include "grid.hpp" -#include -#include - -namespace nd_wfc { - -// Constraint propagator for efficient constraint satisfaction -template -class Propagator { -private: - Wave* wave_; - const ConstraintManager* constraints_; - Grid grid_; - - // Stack for propagation (more cache-friendly than queue for this use case) - mutable std::stack> propagation_stack_; - -public: - Propagator(Wave* wave, const ConstraintManager* constraints) - : wave_(wave), constraints_(constraints), grid_(wave->getSize()) {} - - // Propagate constraints after a cell collapse - bool propagate(const Coord& initial_coord) { - propagation_stack_.push(initial_coord); - - while (!propagation_stack_.empty()) { - Coord coord = propagation_stack_.top(); - propagation_stack_.pop(); - - if (!propagateFromCell(coord)) { - return false; // Contradiction found - } - } - - return true; - } - - // Propagate constraints from a specific cell to its neighbors - bool propagateFromCell(const Coord& coord) { - if (!wave_->isCollapsed(coord)) { - return true; // Nothing to propagate from uncollapsed cell - } - - TileId collapsed_tile = wave_->getCollapsedTile(coord); - auto valid_directions = getValidDirections(); - - // Check each neighbor - for (Direction dir : valid_directions) { - Coord neighbor_coord; - if (!grid_.getNeighbor(coord, dir, neighbor_coord)) { - continue; // No neighbor in this direction - } - - if (wave_->isCollapsed(neighbor_coord)) { - continue; // Neighbor already collapsed - } - - // Get allowed tiles for this direction - TileMask allowed_mask = constraints_->getAllowedTileMask(collapsed_tile, dir); - - // Remove disallowed possibilities from neighbor - if (!constrainNeighbor(neighbor_coord, allowed_mask)) { - return false; // Contradiction - } - } - - return true; - } - - // Constrain a neighbor cell based on allowed tiles - bool constrainNeighbor(const Coord& coord, const TileMask& allowed_mask) { - const TileMask& current_possibilities = wave_->getPossibilities(coord); - TileMask new_possibilities = current_possibilities & allowed_mask; - - // Check if any possibilities were removed - if (new_possibilities == current_possibilities) { - return true; // No change needed - } - - // Check for contradiction - if (!new_possibilities.any()) { - return false; // No possibilities left - } - - // Apply the constraint by removing disallowed tiles - for (Index i = 0; i < constraints_->getNumTiles(); ++i) { - if (current_possibilities[i] && !new_possibilities[i]) { - TileId tile_id = constraints_->getTileId(i); - if (!wave_->removePossibility(coord, tile_id)) { - return false; // Contradiction - } - } - } - - // If the cell now has only one possibility, it's effectively collapsed - if (new_possibilities.count() == 1) { - // Find the remaining tile and collapse to it - for (Index i = 0; i < constraints_->getNumTiles(); ++i) { - if (new_possibilities[i]) { - TileId tile_id = constraints_->getTileId(i); - wave_->collapse(coord, tile_id); - propagation_stack_.push(coord); // Propagate from this newly collapsed cell - break; - } - } - } - - return true; - } - - // Perform full constraint propagation across the entire grid - bool propagateAll() { - // Find all collapsed cells and propagate from them - for (Index i = 0; i < wave_->getTotalSize(); ++i) { - Coord coord = wave_->indexToCoord(i); - if (wave_->isCollapsed(coord)) { - if (!propagateFromCell(coord)) { - return false; - } - } - } - - // Continue propagation until no more changes - while (!propagation_stack_.empty()) { - Coord coord = propagation_stack_.top(); - propagation_stack_.pop(); - - if (!propagateFromCell(coord)) { - return false; - } - } - - return true; - } - - // Check consistency of current wave function state - bool checkConsistency() const { - // Check each cell against its neighbors - for (Index i = 0; i < wave_->getTotalSize(); ++i) { - Coord coord = wave_->indexToCoord(i); - - if (!wave_->isCollapsed(coord)) { - continue; // Skip uncollapsed cells - } - - TileId tile = wave_->getCollapsedTile(coord); - auto valid_directions = getValidDirections(); - - // Check each neighbor - for (Direction dir : valid_directions) { - Coord neighbor_coord; - if (!grid_.getNeighbor(coord, dir, neighbor_coord)) { - continue; // No neighbor - } - - if (!wave_->isCollapsed(neighbor_coord)) { - // Check if any allowed tiles are still possible for the neighbor - TileMask allowed_mask = constraints_->getAllowedTileMask(tile, dir); - const TileMask& neighbor_possibilities = wave_->getPossibilities(neighbor_coord); - - if (!(neighbor_possibilities & allowed_mask).any()) { - return false; // No valid possibilities for neighbor - } - } else { - // Check if the collapsed neighbor is allowed - TileId neighbor_tile = wave_->getCollapsedTile(neighbor_coord); - if (!constraints_->isAllowed(tile, neighbor_tile, dir)) { - return false; // Invalid adjacency - } - } - } - } - - return true; - } - - -}; - -} // namespace nd_wfc diff --git a/include/nd-wfc/types.hpp b/include/nd-wfc/types.hpp deleted file mode 100644 index e54f003..0000000 --- a/include/nd-wfc/types.hpp +++ /dev/null @@ -1,142 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace nd_wfc { - -// Basic types -using Index = std::size_t; -using TileId = std::uint32_t; -using Weight = float; - -// N-dimensional coordinate and size types -template -using Coord = std::array; - -template -using Size = std::array; - -// Direction enumeration for adjacency constraints -enum class Direction : std::uint8_t { - // 2D directions - North = 0, - South = 1, - East = 2, - West = 3, - - // 3D directions (for future use) - Up = 4, - Down = 5, - - // 4D directions (for future use) - Ana = 6, - Kata = 7, - - Count = 8 -}; - -// Get the opposite direction -constexpr Direction getOppositeDirection(Direction dir) { - switch (dir) { - case Direction::North: return Direction::South; - case Direction::South: return Direction::North; - case Direction::East: return Direction::West; - case Direction::West: return Direction::East; - case Direction::Up: return Direction::Down; - case Direction::Down: return Direction::Up; - case Direction::Ana: return Direction::Kata; - case Direction::Kata: return Direction::Ana; - default: return Direction::North; - } -} - -// Get direction offset for n-dimensional coordinates (using signed integers) -template -constexpr std::array getDirectionOffset(Direction dir) { - std::array offset{}; - - if constexpr (N >= 2) { - switch (dir) { - case Direction::North: offset[1] = -1; break; - case Direction::South: offset[1] = 1; break; - case Direction::East: offset[0] = 1; break; - case Direction::West: offset[0] = -1; break; - default: break; - } - } - - if constexpr (N >= 3) { - switch (dir) { - case Direction::Up: offset[2] = 1; break; - case Direction::Down: offset[2] = -1; break; - default: break; - } - } - - if constexpr (N >= 4) { - switch (dir) { - case Direction::Ana: offset[3] = 1; break; - case Direction::Kata: offset[3] = -1; break; - default: break; - } - } - - return offset; -} - -// Get valid directions for N dimensions -template -constexpr std::array getValidDirections() { - if constexpr (N == 2) { - return {Direction::North, Direction::South, Direction::East, Direction::West}; - } else if constexpr (N == 3) { - return {Direction::North, Direction::South, Direction::East, Direction::West, - Direction::Up, Direction::Down}; - } else if constexpr (N == 4) { - return {Direction::North, Direction::South, Direction::East, Direction::West, - Direction::Up, Direction::Down, Direction::Ana, Direction::Kata}; - } else { - static_assert(N <= 4, "Only up to 4 dimensions supported"); - return {}; - } -} - -// Constraint definition -struct Constraint { - TileId from_tile; - TileId to_tile; - Direction direction; - - Constraint(TileId from, TileId to, Direction dir) - : from_tile(from), to_tile(to), direction(dir) {} -}; - -// WFC algorithm result -enum class WfcResult : std::uint8_t { - Success = 0, - Failure = 1, - Contradiction = 2, - MaxIterationsReached = 3, - InvalidInput = 4 -}; - -// WFC configuration -struct WfcConfig { - std::uint32_t seed = 0; - Index max_iterations = 100000; - bool use_entropy_heuristic = true; - bool use_weighted_selection = false; - - WfcConfig() = default; -}; - -// Maximum number of tiles we support (for bitset optimization) -constexpr std::size_t MAX_TILES = 64; - -// Bitset for tracking possible tiles at each cell -using TileMask = std::bitset; - -} // namespace nd_wfc diff --git a/include/nd-wfc/wave.hpp b/include/nd-wfc/wave.hpp deleted file mode 100644 index 8fbb863..0000000 --- a/include/nd-wfc/wave.hpp +++ /dev/null @@ -1,276 +0,0 @@ -#pragma once - -#include "types.hpp" -#include "grid.hpp" -#include -#include - -namespace nd_wfc { - -// Wave function for tracking possible states at each grid cell -template -class Wave { -private: - Size size_; - Index total_size_; - std::vector possibilities_; - std::vector entropy_cache_; - std::vector is_collapsed_; - std::vector tiles_; - Index num_tiles_; - bool entropy_dirty_; - -public: - Wave(const Size& size, const std::vector& tiles) - : size_(size), tiles_(tiles), num_tiles_(tiles.size()), entropy_dirty_(true) { - - assert(num_tiles_ <= MAX_TILES); - - // Calculate total size - total_size_ = 1; - for (std::size_t i = 0; i < N; ++i) { - total_size_ *= size_[i]; - } - - // Initialize all cells with all possibilities - TileMask all_possible; - for (Index i = 0; i < num_tiles_; ++i) { - all_possible.set(i); - } - - possibilities_.resize(total_size_, all_possible); - entropy_cache_.resize(total_size_, static_cast(num_tiles_)); - is_collapsed_.resize(total_size_, false); - } - - // Convert coordinate to linear index - Index coordToIndex(const Coord& coord) const { - Index index = 0; - Index stride = 1; - for (std::size_t i = N; i > 0; --i) { - assert(coord[i-1] < size_[i-1]); - index += coord[i-1] * stride; - stride *= size_[i-1]; - } - return index; - } - - // Convert linear index to coordinate - Coord indexToCoord(Index index) const { - assert(index < total_size_); - Coord coord; - for (std::size_t i = 0; i < N; ++i) { - coord[i] = index % size_[i]; - index /= size_[i]; - } - return coord; - } - - // Get possible tiles at a coordinate - const TileMask& getPossibilities(const Coord& coord) const { - return possibilities_[coordToIndex(coord)]; - } - - // Get possible tiles at an index - const TileMask& getPossibilities(Index index) const { - assert(index < total_size_); - return possibilities_[index]; - } - - // Check if a tile is possible at a coordinate - bool isPossible(const Coord& coord, TileId tile_id) const { - Index tile_index = getTileIndex(tile_id); - return tile_index < num_tiles_ && possibilities_[coordToIndex(coord)][tile_index]; - } - - // Remove a tile possibility from a coordinate - bool removePossibility(const Coord& coord, TileId tile_id) { - Index tile_index = getTileIndex(tile_id); - if (tile_index >= num_tiles_) return false; - - Index cell_index = coordToIndex(coord); - if (!possibilities_[cell_index][tile_index]) { - return false; // Already removed - } - - possibilities_[cell_index][tile_index] = false; - entropy_dirty_ = true; - - // Check for contradiction - return possibilities_[cell_index].any(); - } - - // Collapse a cell to a specific tile - bool collapse(const Coord& coord, TileId tile_id) { - Index tile_index = getTileIndex(tile_id); - if (tile_index >= num_tiles_) return false; - - Index cell_index = coordToIndex(coord); - if (!possibilities_[cell_index][tile_index]) { - return false; // Tile not possible - } - - // Clear all possibilities except the chosen one - possibilities_[cell_index].reset(); - possibilities_[cell_index][tile_index] = true; - is_collapsed_[cell_index] = true; - entropy_dirty_ = true; - - return true; - } - - // Get entropy (number of possible tiles) at a coordinate - std::uint8_t getEntropy(const Coord& coord) const { - Index cell_index = coordToIndex(coord); - if (entropy_dirty_) { - // Recalculate entropy cache - const_cast(this)->updateEntropyCache(); - } - return entropy_cache_[cell_index]; - } - - // Check if a cell is collapsed - bool isCollapsed(const Coord& coord) const { - return is_collapsed_[coordToIndex(coord)]; - } - - // Get the collapsed tile at a coordinate (only valid if collapsed) - TileId getCollapsedTile(const Coord& coord) const { - Index cell_index = coordToIndex(coord); - assert(is_collapsed_[cell_index]); - - const auto& mask = possibilities_[cell_index]; - for (Index i = 0; i < num_tiles_; ++i) { - if (mask[i]) { - return tiles_[i]; - } - } - assert(false); // Should never reach here if properly collapsed - return static_cast(-1); - } - - // Find the cell with minimum entropy (excluding collapsed cells) - bool findMinEntropyCell(Coord& coord, std::mt19937& rng) const { - if (entropy_dirty_) { - const_cast(this)->updateEntropyCache(); - } - - std::uint8_t min_entropy = 255; - std::vector candidates; - - // Find minimum entropy among uncollapsed cells - for (Index i = 0; i < total_size_; ++i) { - if (!is_collapsed_[i]) { - std::uint8_t entropy = entropy_cache_[i]; - if (entropy == 0) { - return false; // Contradiction found - } - if (entropy < min_entropy) { - min_entropy = entropy; - candidates.clear(); - candidates.push_back(i); - } else if (entropy == min_entropy) { - candidates.push_back(i); - } - } - } - - if (candidates.empty()) { - return false; // All cells collapsed or contradiction - } - - // Randomly select among candidates with same entropy - std::uniform_int_distribution dist(0, candidates.size() - 1); - Index chosen_index = candidates[dist(rng)]; - coord = indexToCoord(chosen_index); - - return true; - } - - // Select a random tile from possibilities at a coordinate - TileId selectRandomTile(const Coord& coord, std::mt19937& rng) const { - Index cell_index = coordToIndex(coord); - const auto& mask = possibilities_[cell_index]; - - std::vector available_tiles; - for (Index i = 0; i < num_tiles_; ++i) { - if (mask[i]) { - available_tiles.push_back(tiles_[i]); - } - } - - if (available_tiles.empty()) { - return static_cast(-1); // No possibilities - } - - std::uniform_int_distribution dist(0, available_tiles.size() - 1); - return available_tiles[dist(rng)]; - } - - // Check if the wave function is completely collapsed - bool isComplete() const { - for (Index i = 0; i < total_size_; ++i) { - if (!is_collapsed_[i]) { - return false; - } - } - return true; - } - - // Check for contradictions (cells with no possibilities) - bool hasContradiction() const { - for (Index i = 0; i < total_size_; ++i) { - if (!is_collapsed_[i] && !possibilities_[i].any()) { - return true; - } - } - return false; - } - - // Get the size of the grid - const Size& getSize() const { return size_; } - Index getTotalSize() const { return total_size_; } - - // Reset the wave function to initial state - void reset() { - TileMask all_possible; - for (Index i = 0; i < num_tiles_; ++i) { - all_possible.set(i); - } - - std::fill(possibilities_.begin(), possibilities_.end(), all_possible); - std::fill(entropy_cache_.begin(), entropy_cache_.end(), static_cast(num_tiles_)); - std::fill(is_collapsed_.begin(), is_collapsed_.end(), false); - entropy_dirty_ = true; - } - -private: - // Get the index of a tile ID in the tiles vector - Index getTileIndex(TileId tile_id) const { - for (Index i = 0; i < num_tiles_; ++i) { - if (tiles_[i] == tile_id) { - return i; - } - } - return static_cast(-1); - } - - // Update the entropy cache - void updateEntropyCache() { - for (Index i = 0; i < total_size_; ++i) { - if (is_collapsed_[i]) { - entropy_cache_[i] = 1; - } else { - entropy_cache_[i] = static_cast(possibilities_[i].count()); - } - } - entropy_dirty_ = false; - } -}; - -// Type aliases for common dimensions -using Wave2D = Wave<2>; -using Wave3D = Wave<3>; -using Wave4D = Wave<4>; - -} // namespace nd_wfc diff --git a/include/nd-wfc/wfc.h b/include/nd-wfc/wfc.h new file mode 100644 index 0000000..6f49023 --- /dev/null +++ b/include/nd-wfc/wfc.h @@ -0,0 +1,12 @@ +#pragma once + +/** + * @brief N-Dimensional Wave Function Collapse (WFC) Library + * + * A templated WFC engine that can work with 2D grids, 3D grids, Sudoku, Picross, etc. + * The engine is coordinate-system agnostic and can work with any graph-like structure. + */ + +#include "wfc.hpp" +#include "worlds.hpp" + diff --git a/include/nd-wfc/wfc.hpp b/include/nd-wfc/wfc.hpp index 63d4c1d..b5a88bd 100644 --- a/include/nd-wfc/wfc.hpp +++ b/include/nd-wfc/wfc.hpp @@ -1,186 +1,438 @@ #pragma once -#include "types.hpp" -#include "grid.hpp" -#include "wave.hpp" -#include "constraint.hpp" -#include "entropy.hpp" -#include "propagator.hpp" +#include +#include +#include +#include +#include +#include #include -#include +#include +#include +#include +#include +#include -namespace nd_wfc { +namespace WFC { -// Main Wave Function Collapse algorithm implementation -template -class Wfc { -private: - Size size_; - std::vector tiles_; - ConstraintManager constraints_; - WfcConfig config_; - - // Algorithm state - Wave wave_; - Grid result_grid_; - EntropyCalculator entropy_calc_; - Propagator propagator_; - - // Runtime state - std::mt19937 rng_; - Index iteration_count_; - WfcResult last_result_; - -public: - Wfc(const Size& size, const std::vector& tiles, - const std::vector& constraints, const WfcConfig& config = WfcConfig{}) - : size_(size), tiles_(tiles), constraints_(tiles, constraints), config_(config), - wave_(size, tiles), result_grid_(size), entropy_calc_(&wave_), propagator_(&wave_, &constraints_), - iteration_count_(0), last_result_(WfcResult::InvalidInput) { - - // Initialize random number generator - if (config_.seed == 0) { - auto now = std::chrono::high_resolution_clock::now(); - auto duration = now.time_since_epoch(); - auto seed = std::chrono::duration_cast(duration).count(); - rng_.seed(static_cast(seed)); - } else { - rng_.seed(config_.seed); - } - - // Validate input - if (tiles_.empty()) { - last_result_ = WfcResult::InvalidInput; - } else if (!constraints_.validateConstraints()) { - last_result_ = WfcResult::InvalidInput; - } else { - last_result_ = WfcResult::Success; // Ready to run +inline int FindNthSetBit(size_t num, int n) { + assert(!(n <= 0 || static_cast(n) > std::popcount(num))); + int bitCount = 0; + while (num) { + bitCount++; + if (bitCount == n) { + return std::countr_zero(num); // Index of the current set bit } + num &= (num - 1); } + return -1; +} - // Run the WFC algorithm - WfcResult run() { - - // Reset state - wave_.reset(); - result_grid_.clear(); - iteration_count_ = 0; - entropy_calc_.markDirty(); - - // Initial constraint propagation - if (!propagator_.propagateAll()) { - last_result_ = WfcResult::Contradiction; - return last_result_; - } - - // Main algorithm loop - while (!wave_.isComplete() && iteration_count_ < config_.max_iterations) { - ++iteration_count_; - - // Find cell with minimum entropy - Coord coord; - if (!entropy_calc_.findMinEntropyCell(coord, rng_)) { - if (wave_.hasContradiction()) { - last_result_ = WfcResult::Contradiction; - } else { - last_result_ = WfcResult::Success; // All cells collapsed - } - break; - } - - // Select a random tile from possibilities - TileId selected_tile = wave_.selectRandomTile(coord, rng_); - if (selected_tile == static_cast(-1)) { - last_result_ = WfcResult::Contradiction; - break; - } - - // Collapse the cell - if (!wave_.collapse(coord, selected_tile)) { - last_result_ = WfcResult::Contradiction; - break; - } - - // Propagate constraints - if (!propagator_.propagate(coord)) { - last_result_ = WfcResult::Contradiction; - break; - } - - // Mark entropy cache as dirty - entropy_calc_.markDirty(); - } - - // Check final state - if (iteration_count_ >= config_.max_iterations) { - last_result_ = WfcResult::MaxIterationsReached; - } else if (last_result_ != WfcResult::Contradiction && wave_.isComplete()) { - last_result_ = WfcResult::Success; - copyWaveToGrid(); - } - - return last_result_; - } - - // Get the result grid (only valid after successful run) - const Grid& getGrid() const { return result_grid_; } - - // Check if the algorithm completed successfully - bool isComplete() const { - return last_result_ == WfcResult::Success && wave_.isComplete(); - } - - // Get iteration count from last run - Index getIterationCount() const { return iteration_count_; } - - // Get last result - WfcResult getLastResult() const { return last_result_; } - - // Get entropy statistics - typename EntropyCalculator::EntropyStats getEntropyStats() const { - return entropy_calc_.getStats(); - } - - // Reset and prepare for another run - void reset() { - wave_.reset(); - result_grid_.clear(); - iteration_count_ = 0; - entropy_calc_.markDirty(); - last_result_ = WfcResult::InvalidInput; - } - - // Get the current wave state (for debugging) - const Wave& getWave() const { return wave_; } - - // Get configuration - const WfcConfig& getConfig() const { return config_; } - - // Update configuration - void setConfig(const WfcConfig& config) { - config_ = config; - if (config_.seed != 0) { - rng_.seed(config_.seed); - } - } - -private: - // Copy the final wave state to the result grid - void copyWaveToGrid() { - for (Index i = 0; i < wave_.getTotalSize(); ++i) { - Coord coord = wave_.indexToCoord(i); - if (wave_.isCollapsed(coord)) { - result_grid_[coord] = wave_.getCollapsedTile(coord); - } else { - result_grid_[coord] = static_cast(-1); // Should not happen on success - } - } - } +template +concept WorldType = requires(T world, size_t id, typename T::ValueType value) { + { world.size() } -> std::convertible_to; + { world.setValue(id, value) }; + typename T::ValueType; }; -// Type aliases for common dimensions -using Wfc2D = Wfc<2>; -using Wfc3D = Wfc<3>; -using Wfc4D = Wfc<4>; +template +class Wave; +template +class Constrainer; +template +class WFC; +template +class Variable; +template +struct WorldValue; -} // namespace nd_wfc +/** + * @brief Class to map variable values to indices at compile time + * + * This class is used to map variable values to indices at compile time. + * It is a compile-time map of variable values to indices. + */ +template +class VariableIDMap { +public: + + using Type = VarT; + static constexpr size_t ValuesRegisteredAmount = sizeof...(Values); + + using MaskType = typename std::conditional< + ValuesRegisteredAmount <= 8, + uint8_t, + typename std::conditional< + ValuesRegisteredAmount <= 16, + uint16_t, + typename std::conditional< + ValuesRegisteredAmount <= 32, + uint32_t, + uint64_t + >::type + >::type + >::type; + + template + using Merge = VariableIDMap; + + template + static consteval bool HasValue() + { + constexpr VarT arr[] = {Values...}; + constexpr size_t size = sizeof...(Values); + + for (size_t i = 0; i < size; ++i) + if (arr[i] == Value) + return true; + return false; + } + + template + static consteval size_t GetIndex() + { + static_assert(HasValue(), "Value was not defined"); + constexpr VarT arr[] = {Values...}; + constexpr size_t size = sizeof...(Values); + + for (size_t i = 0; i < size; ++i) + if (arr[i] == Value) + return i; + + return static_cast(-1); // This line is unreachable if value is found + } + + static VarT GetValue(size_t index) { + constexpr VarT arr[] = {Values...}; + return arr[index]; + } + + template + static consteval MaskType GetMask() + { + return (0 | ... | (1 << GetIndex())); + } + + static consteval size_t size() { return sizeof...(Values); } +}; + +template +struct WorldValue +{ +public: + WorldValue() = default; + WorldValue(VarT value, uint16_t internalIndex) + : Value(value) + , InternalIndex(internalIndex) + {} +public: + operator VarT() const { return Value; } + +public: + VarT Value{}; + uint16_t InternalIndex{}; +}; + +template +class Wave { +public: + Wave() = default; + Wave(size_t size, size_t variableAmount) : m_data(size) + { + for (auto& wave : m_data) wave = (1 << variableAmount) - 1; + } + + void Collapse(size_t index, MaskType mask) { m_data[index] &= mask; } + size_t size() const { return m_data.size(); } + size_t Entropy(size_t index) const { return std::popcount(m_data[index]); } + bool IsCollapsed(size_t index) const { return Entropy(index) == 1; } + bool IsFullyCollapsed() const { return std::all_of(m_data.begin(), m_data.end(), [](MaskType value) { return std::popcount(value) == 1; }); } + bool HasContradiction() const { return std::any_of(m_data.begin(), m_data.end(), [](MaskType value) { return value == 0; }); } + bool IsContradicted(size_t index) const { return m_data[index] == 0; } + uint16_t GetVariableID(size_t index) const { return static_cast(std::countr_zero(m_data[index])); } + MaskType GetMask(size_t index) const { return m_data[index]; } + +private: + std::vector m_data; +}; + +/** + * @brief Constrainer class used in constraint functions to limit possible values for other cells + */ +template +class Constrainer { +public: + using MaskType = typename VariableIDMapT::MaskType; + +public: + Constrainer(Wave& wave, std::queue& propagationQueue) + : m_wave(wave) + , m_propagationQueue(propagationQueue) + {} + + /** + * @brief Constrain a cell to exclude specific values + * @param cellId The ID of the cell to constrain + * @param forbiddenValues The set of forbidden values for this cell + */ + template + void Exclude(size_t cellId) { + static_assert(sizeof...(ExcludedValues) > 0, "At least one excluded value must be provided"); + ApplyMask(cellId, ~VariableIDMapT::template GetMask()); + } + + void Exclude(WorldValue value, size_t cellId) { + ApplyMask(cellId, ~value.InternalIndex); + } + + /** + * @brief Constrain a cell to only allow one specific value + * @param cellId The ID of the cell to constrain + * @param value The only allowed value for this cell + */ + template + void Only(size_t cellId) { + static_assert(sizeof...(AllowedValues) > 0, "At least one allowed value must be provided"); + ApplyMask(cellId, VariableIDMapT::template GetMask()); + } + + void Only(WorldValue value, size_t cellId) { + ApplyMask(cellId, value.InternalIndex); + } + +private: + void ApplyMask(size_t cellId, MaskType mask) { + bool collapsedBefore = m_wave.IsCollapsed(cellId); + + m_wave.Collapse(cellId, mask); + + bool collapsedAfter = m_wave.IsCollapsed(cellId); + if (!collapsedBefore && collapsedAfter) { + m_propagationQueue.push(cellId); + } + } + +private: + Wave& m_wave; + std::queue& m_propagationQueue; +}; + +/** + * @brief Variable definition with its constraint function + */ +template +struct VariableData { + VarT value{}; + std::function, Constrainer&)> constraintFunc{}; + + VariableData() = default; + VariableData(VarT value, std::function, Constrainer&)> constraintFunc) + : value(value) + , constraintFunc(constraintFunc) + {} +}; + +/** + * @brief Main WFC class implementing the Wave Function Collapse algorithm + */ +template> +class WFC { +public: + static_assert(WorldType, "WorldT must satisfy World type requirements"); + + using MaskType = typename VariableIDMapT::MaskType; + +public: + struct WorldSolver { + WorldT& world; + std::queue propagationQueue; + Wave wave; + std::mt19937 rng; + + WorldSolver(WorldT& world, const std::vector>& variables) + : world(world) + , propagationQueue() + , wave(world.size(), variables.size()) + , rng(std::random_device{}()) + {} + }; + +public: + WFC(std::vector>&& variables) + : m_variables(std::move(variables)) + {} + +public: + bool Run(WorldT& world) + { + WorldSolver worldSolver(world, m_variables); + return Run(worldSolver); + } + + /** + * @brief Run the WFC algorithm to generate a solution + * @return true if a solution was found, false if contradiction occurred + */ + bool Run(WorldSolver& worldSolver) + { + for (size_t i = 0; i < 1024; ++i) + { + Propagate(worldSolver); + + if (worldSolver.wave.IsFullyCollapsed()) { + PopulateWorld(worldSolver); + return true; + } else if (worldSolver.wave.HasContradiction()) { + return false; + } else { + GetMinEntropyCell(worldSolver); + } + } + return true; + } + + /** + * @brief Get the value at a specific cell + * @param cellId The cell ID + * @return The value if collapsed, std::nullopt otherwise + */ + std::optional GetValue(WorldSolver& worldSolver, int cellId) const { + if (worldSolver.wave.IsCollapsed(cellId)) { + auto variableId = worldSolver.wave.GetVariableID(cellId); + return VariableIDMapT::GetValue(variableId); + } + return std::nullopt; + } + + /** + * @brief Get all possible values for a cell + * @param cellId The cell ID + * @return Set of possible values + */ + const std::vector GetPossibleValues(WorldSolver& worldSolver, int cellId) const + { + std::vector possibleValues; + MaskType mask = worldSolver.wave.GetMask(cellId); + for (size_t i = 0; i < m_variables.size(); ++i) { + if (mask & (1 << i)) possibleValues.push_back(VariableIDMapT::GetValue(i)); + } + return possibleValues; + } + +private: + bool GetMinEntropyCell(WorldSolver& worldSolver) + { + assert(worldSolver.propagationQueue.empty()); + + // Find cell with minimum entropy > 1 + size_t minEntropyCell = static_cast(-1); + size_t minEntropy = static_cast(-1); + + for (size_t i = 0; i < worldSolver.wave.size(); ++i) { + size_t entropy = worldSolver.wave.Entropy(i); + if (entropy > 1 && entropy < minEntropy) { + minEntropy = entropy; + minEntropyCell = i; + } + } + + // Randomly select a value from possible values + size_t availableValues = worldSolver.wave.Entropy(minEntropyCell); + std::uniform_int_distribution dist(0, availableValues - 1); + size_t selectedValue = FindNthSetBit(worldSolver.wave.GetMask(minEntropyCell), dist(worldSolver.rng)); + + // Collapse the cell to the selected value + worldSolver.wave.Collapse(minEntropyCell, 1 << selectedValue); + + worldSolver.propagationQueue.push(minEntropyCell); + + return true; + } + + void Propagate(WorldSolver& worldSolver) + { + while (!worldSolver.propagationQueue.empty()) + { + size_t cellId = worldSolver.propagationQueue.front(); + worldSolver.propagationQueue.pop(); + + assert(worldSolver.wave.IsCollapsed(cellId)); + + uint16_t variableID = worldSolver.wave.GetVariableID(cellId); + Constrainer constrainer(worldSolver.wave, worldSolver.propagationQueue); + m_variables[variableID].constraintFunc(worldSolver.world, cellId, WorldValue{VariableIDMapT::GetValue(variableID), variableID}, constrainer); + } + } + + void PopulateWorld(WorldSolver& worldSolver) + { + for (size_t i = 0; i < worldSolver.wave.size(); ++i) + { + worldSolver.world.setValue(i, VariableIDMapT::GetValue(worldSolver.wave.GetVariableID(i))); + } + } + + std::vector> m_variables {}; +}; + +/** + * @brief Builder class for creating WFC instances + */ +template> +class Builder { +public: + Builder() = default; + Builder(std::vector>&& variables) + : m_variables(std::move(variables)) + {} + +public: + template + auto DefineIDs() + { + using NewVariableIDMapT = typename VariableIDMapT::template Merge; + // reinterpret_cast is used to be able to move the variables with an outdated VariableIDMap to the new VariableIDMap. The previous indices still work. + return Builder(std::move(reinterpret_cast>&>(m_variables))); + } + + /** + * @brief Add a variable with its constraint function + * @param value The variable value + * @param constraintFunc Function that defines constraints when this variable is placed + * @return Reference to this builder for method chaining + */ + template + Builder& Variable(const std::function, Constrainer&)> constraintFunc) { + m_variables.resize(VariableIDMapT::ValuesRegisteredAmount); + + Variable_Internal(constraintFunc); + return *this; + } + + /** + * @brief Build the WFC instance + * @param world The world instance to work with + * @return A unique_ptr to the created WFC instance + */ + auto build() { + return WFC(std::move(m_variables)); + } + +private: + template + void Variable_Internal(const std::function, Constrainer&)> constraintFunc) + { + m_variables[VariableIDMapT::template GetIndex()] = VariableData{ + Value, + constraintFunc + }; + if constexpr (sizeof...(Values) > 0) { + Variable_Internal(constraintFunc); + } + } + +private: + std::vector> m_variables; +}; + +} // namespace WFC diff --git a/include/nd-wfc/worlds.hpp b/include/nd-wfc/worlds.hpp new file mode 100644 index 0000000..30cc416 --- /dev/null +++ b/include/nd-wfc/worlds.hpp @@ -0,0 +1,169 @@ +#pragma once + +#include +#include +#include + +namespace WFC { + +/** + * @brief 2D Array World implementation + */ +template +class Array2D { +public: + using ValueType = T; + using CoordType = std::tuple; + + Array2D() = default; + + /** + * @brief Get the total size of the world + */ + size_t size() const { + return Width * Height; + } + + /** + * @brief Convert coordinates to cell ID + */ + int getId(CoordType coord) const { + auto [x, y] = coord; + return y * Width + x; + } + + /** + * @brief Convert cell ID to coordinates + */ + CoordType getCoord(int id) const { + int x = id % Width; + int y = id / Width; + return {x, y}; + } + + /** + * @brief Get width of the array + */ + size_t width() const { return Width; } + + /** + * @brief Get height of the array + */ + size_t height() const { return Height; } + + /** + * @brief Access element at coordinates + */ + T& at(int x, int y) { + return data_[y * Width + x]; + } + + /** + * @brief Access element at coordinates (const) + */ + const T& at(int x, int y) const { + return data_[y * Width + x]; + } + + /** + * @brief Access element by ID + */ + T& operator[](int id) { + return data_[id]; + } + + /** + * @brief Access element by ID (const) + */ + const T& operator[](int id) const { + return data_[id]; + } + + /** + * @brief Set value at specific index (required by WFC) + */ + void setValue(size_t index, T value) { + data_[index] = value; + } + +private: + std::array data_; +}; + +/** + * @brief 3D Array World implementation + */ +template +class Array3D { +public: + using ValueType = T; + using CoordType = std::tuple; + + Array3D() = default; + + /** + * @brief Get the total size of the world + */ + size_t size() const { + return Width * Height * Depth; + } + + /** + * @brief Convert coordinates to cell ID + */ + int getId(CoordType coord) const { + auto [x, y, z] = coord; + return z * (Width * Height) + y * Width + x; + } + + /** + * @brief Convert cell ID to coordinates + */ + CoordType getCoord(int id) const { + int x = id % Width; + int y = (id / Width) % Height; + int z = id / (Width * Height); + return {x, y, z}; + } + + /** + * @brief Access element at coordinates + */ + T& at(int x, int y, int z) { + return data_[z * (Width * Height) + y * Width + x]; + } + + /** + * @brief Access element at coordinates (const) + */ + const T& at(int x, int y, int z) const { + return data_[z * (Width * Height) + y * Width + x]; + } + + /** + * @brief Access element by ID + */ + T& operator[](int id) { + return data_[id]; + } + + /** + * @brief Access element by ID (const) + */ + const T& operator[](int id) const { + return data_[id]; + } + + /** + * @brief Set value at specific index (required by WFC) + */ + void setValue(size_t index, T value) { + data_[index] = value; + } + +private: + std::array data_; +}; + +} // namespace WFC + diff --git a/prompts/4-abstract-wfc b/prompts/4-abstract-wfc new file mode 100644 index 0000000..8eccdd5 --- /dev/null +++ b/prompts/4-abstract-wfc @@ -0,0 +1,46 @@ +In this repo I want you to implement a templated Wave Function Collapse engine that would be compatible with 2D grids, 3D grids, Sudoku, Picross, etc. +I want the following to be possible: + +WFC::Builder() + .variable(name/id/userdata, ...).constrains([](World& world, int worldID, Constrainer& constrainer, name/id/userdata){ constrainer.constrain(world.getid(...)); }) + .variable(...) + +The World is responsible for giving its size and giving ids. the ids should all fit in the size of the world. +when setting the variable on a node/cell, it should call the lambda to constrain the rest of the unknown nodes/cells. +The WFC should have no concept of coordinate systems. The WFC algorithm should be able to work even without coordinates, and just with a graph for example. + +sudoku eg: +WFC::Builder>() + .variable(1,2,3,4,5,6,7,8,9).constrains([](Array2D& world, int worldID, Constrainer& constrainer, uint8_t var) + { + int [x,y] = world.getCoord(worldID); + + for (int i{}; i < 9; ++i) + { + constrainer.constrain(9 * y + i, var); + constrainer.constrain(i * y + x, var); + // TODO add small square constraint + } + }) + +2D tilemap eg: +WFC::Builder>() + .variable(SEA).constrains([](Array2D& world, int worldID, Constrainer& constrainer, uint8_t var) + { + int [x,y] = world.getCoord(worldID); + + constrainer.only(y*100 + x - 1, SEA, BEACH); + constrainer.only(y*99 + x, SEA, BEACH); + constrainer.only(x*101 + x, SEA, BEACH); + constrainer.only(x*100 + x + 1, SEA, BEACH); + }) + .variable(BEACH).constrains([](Array2D& world, int worldID, Constrainer& constrainer, uint8_t var) + { + int [x,y] = world.getCoord(worldID); + + constrainer.only(y*100 + x - 1, SEA, LAND); + constrainer.only(y*99 + x, SEA, LAND); + constrainer.only(x*101 + x, SEA, LAND); + constrainer.only(x*100 + x + 1, SEA, LAND); + }) + ... \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index eaab10c..fbb0431 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,248 +1,6 @@ - - +#include #include -#include -#include -#include "nd-wfc/wfc.hpp" -#include "nd-wfc/types.hpp" - -void printGrid2D(const nd_wfc::Grid2D& grid) { - const auto& size = grid.getSize(); - std::cout << "\nGenerated 2D Grid (" << size[0] << "x" << size[1] << "):\n"; - - for (nd_wfc::Index y = 0; y < size[1]; ++y) { - for (nd_wfc::Index x = 0; x < size[0]; ++x) { - nd_wfc::Coord<2> coord = {x, y}; - nd_wfc::TileId tile = grid[coord]; - - char symbol; - switch (tile) { - case 0: symbol = '.'; break; // Empty - case 1: symbol = 'G'; break; // Grass - case 2: symbol = 'W'; break; // Water - case 3: symbol = 'M'; break; // Mountain - default: symbol = '?'; break; - } - std::cout << symbol << ' '; - } - std::cout << '\n'; - } - std::cout << std::endl; -} - -void test2DSimple() { - std::cout << "=== Testing 2D WFC - Simple Terrain ===\n"; - - // Create a 8x8 grid - nd_wfc::Size<2> size = {8, 8}; - - // Define 4 tile types: Empty(0), Grass(1), Water(2), Mountain(3) - std::vector tiles = {0, 1, 2, 3}; - - // Define adjacency constraints for terrain generation - std::vector constraints = { - // Empty can connect to anything - {0, 0, nd_wfc::Direction::North}, {0, 0, nd_wfc::Direction::South}, - {0, 0, nd_wfc::Direction::East}, {0, 0, nd_wfc::Direction::West}, - {0, 1, nd_wfc::Direction::North}, {0, 1, nd_wfc::Direction::South}, - {0, 1, nd_wfc::Direction::East}, {0, 1, nd_wfc::Direction::West}, - {0, 2, nd_wfc::Direction::North}, {0, 2, nd_wfc::Direction::South}, - {0, 2, nd_wfc::Direction::East}, {0, 2, nd_wfc::Direction::West}, - {0, 3, nd_wfc::Direction::North}, {0, 3, nd_wfc::Direction::South}, - {0, 3, nd_wfc::Direction::East}, {0, 3, nd_wfc::Direction::West}, - - // Grass can connect to Empty, Grass, and Mountain (not Water) - {1, 0, nd_wfc::Direction::North}, {1, 0, nd_wfc::Direction::South}, - {1, 0, nd_wfc::Direction::East}, {1, 0, nd_wfc::Direction::West}, - {1, 1, nd_wfc::Direction::North}, {1, 1, nd_wfc::Direction::South}, - {1, 1, nd_wfc::Direction::East}, {1, 1, nd_wfc::Direction::West}, - {1, 3, nd_wfc::Direction::North}, {1, 3, nd_wfc::Direction::South}, - {1, 3, nd_wfc::Direction::East}, {1, 3, nd_wfc::Direction::West}, - - // Water can only connect to Empty and Water - {2, 0, nd_wfc::Direction::North}, {2, 0, nd_wfc::Direction::South}, - {2, 0, nd_wfc::Direction::East}, {2, 0, nd_wfc::Direction::West}, - {2, 2, nd_wfc::Direction::North}, {2, 2, nd_wfc::Direction::South}, - {2, 2, nd_wfc::Direction::East}, {2, 2, nd_wfc::Direction::West}, - - // Mountain can connect to Empty, Grass, and Mountain (not Water) - {3, 0, nd_wfc::Direction::North}, {3, 0, nd_wfc::Direction::South}, - {3, 0, nd_wfc::Direction::East}, {3, 0, nd_wfc::Direction::West}, - {3, 1, nd_wfc::Direction::North}, {3, 1, nd_wfc::Direction::South}, - {3, 1, nd_wfc::Direction::East}, {3, 1, nd_wfc::Direction::West}, - {3, 3, nd_wfc::Direction::North}, {3, 3, nd_wfc::Direction::South}, - {3, 3, nd_wfc::Direction::East}, {3, 3, nd_wfc::Direction::West}, - }; - - nd_wfc::WfcConfig config; - config.seed = 12345; - config.max_iterations = 10000; - - nd_wfc::Wfc2D wfc(size, tiles, constraints, config); - - auto start_time = std::chrono::high_resolution_clock::now(); - nd_wfc::WfcResult result = wfc.run(); - auto end_time = std::chrono::high_resolution_clock::now(); - - auto duration = std::chrono::duration_cast(end_time - start_time); - - std::cout << "Result: "; - switch (result) { - case nd_wfc::WfcResult::Success: - std::cout << "Success!\n"; - break; - case nd_wfc::WfcResult::Failure: - std::cout << "Failure\n"; - break; - case nd_wfc::WfcResult::Contradiction: - std::cout << "Contradiction\n"; - break; - case nd_wfc::WfcResult::MaxIterationsReached: - std::cout << "Max iterations reached\n"; - break; - case nd_wfc::WfcResult::InvalidInput: - std::cout << "Invalid input\n"; - break; - } - - std::cout << "Iterations: " << wfc.getIterationCount() << "\n"; - std::cout << "Time: " << duration.count() << " microseconds\n"; - - if (result == nd_wfc::WfcResult::Success) { - printGrid2D(wfc.getGrid()); - - auto stats = wfc.getEntropyStats(); - std::cout << "Final entropy stats - Min: " << static_cast(stats.min_entropy) - << ", Max: " << static_cast(stats.max_entropy) - << ", Uncollapsed: " << stats.uncollapsed_cells << "\n"; - } -} - -void test2DConstrainedPattern() { - std::cout << "\n=== Testing 2D WFC - Constrained Pattern ===\n"; - - // Create a smaller grid for detailed analysis - nd_wfc::Size<2> size = {5, 5}; - - // Two tile types: 0 (empty), 1 (filled) - std::vector tiles = {0, 1}; - - // Constraints: create a checkerboard-like pattern - std::vector constraints = { - // Tile 0 can only connect to tile 1 - {0, 1, nd_wfc::Direction::North}, {0, 1, nd_wfc::Direction::South}, - {0, 1, nd_wfc::Direction::East}, {0, 1, nd_wfc::Direction::West}, - - // Tile 1 can only connect to tile 0 - {1, 0, nd_wfc::Direction::North}, {1, 0, nd_wfc::Direction::South}, - {1, 0, nd_wfc::Direction::East}, {1, 0, nd_wfc::Direction::West}, - }; - - nd_wfc::WfcConfig config; - config.seed = 54321; - config.max_iterations = 1000; - - nd_wfc::Wfc2D wfc(size, tiles, constraints, config); - - auto start_time = std::chrono::high_resolution_clock::now(); - nd_wfc::WfcResult result = wfc.run(); - auto end_time = std::chrono::high_resolution_clock::now(); - - auto duration = std::chrono::duration_cast(end_time - start_time); - - std::cout << "Result: "; - switch (result) { - case nd_wfc::WfcResult::Success: - std::cout << "Success!\n"; - break; - case nd_wfc::WfcResult::Contradiction: - std::cout << "Contradiction (expected for checkerboard on 5x5)\n"; - break; - default: - std::cout << "Other: " << static_cast(result) << "\n"; - break; - } - - std::cout << "Iterations: " << wfc.getIterationCount() << "\n"; - std::cout << "Time: " << duration.count() << " microseconds\n"; - - if (result == nd_wfc::WfcResult::Success) { - printGrid2D(wfc.getGrid()); - } -} int main() { - std::cout << "N-Dimensional Wave Function Collapse - Performance Test\n"; - std::cout << "======================================================\n"; - try { - test2DSimple(); - test2DConstrainedPattern(); - - // Performance test with larger grid - std::cout << "\n=== Performance Test - Large Grid ===\n"; - nd_wfc::Size<2> large_size = {20, 20}; - std::vector tiles = {0, 1, 2, 3}; - - // Same terrain constraints as before - std::vector constraints = { - {0, 0, nd_wfc::Direction::North}, {0, 0, nd_wfc::Direction::South}, - {0, 0, nd_wfc::Direction::East}, {0, 0, nd_wfc::Direction::West}, - {0, 1, nd_wfc::Direction::North}, {0, 1, nd_wfc::Direction::South}, - {0, 1, nd_wfc::Direction::East}, {0, 1, nd_wfc::Direction::West}, - {0, 2, nd_wfc::Direction::North}, {0, 2, nd_wfc::Direction::South}, - {0, 2, nd_wfc::Direction::East}, {0, 2, nd_wfc::Direction::West}, - {0, 3, nd_wfc::Direction::North}, {0, 3, nd_wfc::Direction::South}, - {0, 3, nd_wfc::Direction::East}, {0, 3, nd_wfc::Direction::West}, - {1, 0, nd_wfc::Direction::North}, {1, 0, nd_wfc::Direction::South}, - {1, 0, nd_wfc::Direction::East}, {1, 0, nd_wfc::Direction::West}, - {1, 1, nd_wfc::Direction::North}, {1, 1, nd_wfc::Direction::South}, - {1, 1, nd_wfc::Direction::East}, {1, 1, nd_wfc::Direction::West}, - {1, 3, nd_wfc::Direction::North}, {1, 3, nd_wfc::Direction::South}, - {1, 3, nd_wfc::Direction::East}, {1, 3, nd_wfc::Direction::West}, - {2, 0, nd_wfc::Direction::North}, {2, 0, nd_wfc::Direction::South}, - {2, 0, nd_wfc::Direction::East}, {2, 0, nd_wfc::Direction::West}, - {2, 2, nd_wfc::Direction::North}, {2, 2, nd_wfc::Direction::South}, - {2, 2, nd_wfc::Direction::East}, {2, 2, nd_wfc::Direction::West}, - {3, 0, nd_wfc::Direction::North}, {3, 0, nd_wfc::Direction::South}, - {3, 0, nd_wfc::Direction::East}, {3, 0, nd_wfc::Direction::West}, - {3, 1, nd_wfc::Direction::North}, {3, 1, nd_wfc::Direction::South}, - {3, 1, nd_wfc::Direction::East}, {3, 1, nd_wfc::Direction::West}, - {3, 3, nd_wfc::Direction::North}, {3, 3, nd_wfc::Direction::South}, - {3, 3, nd_wfc::Direction::East}, {3, 3, nd_wfc::Direction::West}, - }; - - nd_wfc::WfcConfig config; - config.seed = 98765; - config.max_iterations = 50000; - - nd_wfc::Wfc2D large_wfc(large_size, tiles, constraints, config); - - auto start_time = std::chrono::high_resolution_clock::now(); - nd_wfc::WfcResult result = large_wfc.run(); - auto end_time = std::chrono::high_resolution_clock::now(); - - auto duration = std::chrono::duration_cast(end_time - start_time); - - std::cout << "Large grid (20x20) result: "; - switch (result) { - case nd_wfc::WfcResult::Success: std::cout << "Success!\n"; break; - case nd_wfc::WfcResult::Contradiction: std::cout << "Contradiction\n"; break; - case nd_wfc::WfcResult::MaxIterationsReached: std::cout << "Max iterations\n"; break; - default: std::cout << "Other: " << static_cast(result) << "\n"; break; - } - - std::cout << "Iterations: " << large_wfc.getIterationCount() << "\n"; - std::cout << "Time: " << duration.count() << " milliseconds\n"; - std::cout << "Performance: " << (large_wfc.getIterationCount() * 1000.0 / duration.count()) - << " iterations/second\n"; - - std::cout << "\nAll tests completed!\n"; - - } catch (const std::exception& e) { - std::cerr << "Error: " << e.what() << std::endl; - return 1; - } - - return 0; } \ No newline at end of file diff --git a/tests/test_wfc.cpp b/tests/test_wfc.cpp deleted file mode 100644 index e87381a..0000000 --- a/tests/test_wfc.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include -#include "nd-wfc/wfc.hpp" -#include "nd-wfc/types.hpp" - -namespace nd_wfc { - -// Basic test for 2D WFC -TEST(WfcTest, Basic2D) { - // Create a simple 2x2 grid with 2 tile types - Size<2> size = {2, 2}; - std::vector tiles = {0, 1}; - - // Simple constraints: tile 0 can connect to tile 0 and 1 in all directions - // tile 1 can only connect to tile 1 - std::vector constraints = { - {0, 0, Direction::North}, {0, 0, Direction::South}, - {0, 0, Direction::East}, {0, 0, Direction::West}, - {0, 1, Direction::North}, {0, 1, Direction::South}, - {0, 1, Direction::East}, {0, 1, Direction::West}, - {1, 1, Direction::North}, {1, 1, Direction::South}, - {1, 1, Direction::East}, {1, 1, Direction::West} - }; - - WfcConfig config; - config.seed = 42; // For reproducible tests - - Wfc2D wfc(size, tiles, constraints, config); - - WfcResult result = wfc.run(); - - // The algorithm should succeed - EXPECT_EQ(result, WfcResult::Success); - EXPECT_TRUE(wfc.isComplete()); -} - -// Test grid functionality -TEST(GridTest, Basic2D) { - Size<2> size = {3, 3}; - Grid<2> grid(size); - - // Test coordinate conversion - Coord<2> coord = {1, 2}; - Index index = grid.coordToIndex(coord); - Coord<2> back_coord = grid.indexToCoord(index); - - EXPECT_EQ(coord, back_coord); - EXPECT_EQ(grid.getSize(), size); - EXPECT_EQ(grid.getTotalSize(), 9); -} - -} // namespace nd_wfc diff --git a/thirdparty/CMakeLists.txt b/thirdparty/CMakeLists.txt deleted file mode 100644 index 0517910..0000000 --- a/thirdparty/CMakeLists.txt +++ /dev/null @@ -1,80 +0,0 @@ -# Third-party dependencies - optional components for future features -# For now, we'll make these optional since the core WFC doesn't need them - -# RapidJSON - header-only library (optional) -if(ND_WFC_USE_SYSTEM_LIBS) - find_package(RapidJSON QUIET) - if(RapidJSON_FOUND) - message(STATUS "Found system RapidJSON") - endif() -else() - # Check if rapidjson submodule exists - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/rapidjson/CMakeLists.txt") - set(RAPIDJSON_BUILD_EXAMPLES OFF CACHE BOOL "Build RapidJSON examples" FORCE) - set(RAPIDJSON_BUILD_TESTS OFF CACHE BOOL "Build RapidJSON tests" FORCE) - set(RAPIDJSON_BUILD_DOC OFF CACHE BOOL "Build RapidJSON documentation" FORCE) - add_subdirectory(rapidjson) - message(STATUS "Using bundled RapidJSON") - else() - message(STATUS "RapidJSON not found - skipping (optional dependency)") - endif() -endif() - -# Assimp - 3D model loading (optional) -if(ND_WFC_USE_SYSTEM_LIBS) - find_package(assimp QUIET) - if(assimp_FOUND) - message(STATUS "Found system Assimp") - endif() -else() - # Check if assimp submodule exists - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/assimp/CMakeLists.txt") - set(ASSIMP_BUILD_TESTS OFF CACHE BOOL "Build Assimp tests" FORCE) - set(ASSIMP_BUILD_ASSIMP_TOOLS OFF CACHE BOOL "Build Assimp tools" FORCE) - set(ASSIMP_BUILD_SAMPLES OFF CACHE BOOL "Build Assimp samples" FORCE) - set(ASSIMP_BUILD_DOCS OFF CACHE BOOL "Build Assimp documentation" FORCE) - set(ASSIMP_INSTALL OFF CACHE BOOL "Install Assimp" FORCE) - add_subdirectory(assimp) - message(STATUS "Using bundled Assimp") - else() - message(STATUS "Assimp not found - skipping (optional dependency)") - endif() -endif() - -# SDL3 - for graphics and windowing (optional) -if(ND_WFC_USE_SYSTEM_LIBS) - find_package(SDL3 QUIET) - if(SDL3_FOUND) - message(STATUS "Found system SDL3") - endif() -else() - # Check if SDL submodule exists - if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/SDL/CMakeLists.txt") - set(SDL_SHARED OFF CACHE BOOL "Build SDL as a shared library" FORCE) - set(SDL_STATIC ON CACHE BOOL "Build SDL as a static library" FORCE) - set(SDL_TEST OFF CACHE BOOL "Build SDL tests" FORCE) - add_subdirectory(SDL) - message(STATUS "Using bundled SDL3") - else() - message(STATUS "SDL3 not found - skipping (optional dependency)") - endif() -endif() - -# SDL_image - for image loading (temporarily disabled due to SDL3 dependency issue) -# TODO: Re-enable SDL_image once SDL3 dependency resolution is fixed -# if(ND_WFC_USE_SYSTEM_LIBS) -# find_package(SDL3_image REQUIRED) -# else() -# # Use the git submodule -# # SDL_image should automatically find the SDL3 targets we just built -# set(SDL3IMAGE_SHARED OFF CACHE BOOL "Build SDL_image as a shared library" FORCE) -# set(SDL3IMAGE_STATIC ON CACHE BOOL "Build SDL_image as a static library" FORCE) -# set(SDL3IMAGE_TESTS OFF CACHE BOOL "Build SDL_image tests" FORCE) -# set(SDL3IMAGE_SAMPLES OFF CACHE BOOL "Build SDL_image samples" FORCE) -# set(SDL3IMAGE_DEPS_SHARED OFF CACHE BOOL "Build SDL_image dependencies as shared libraries" FORCE) -# set(SDL3IMAGE_VENDORED OFF CACHE BOOL "Use vendored libraries" FORCE) -# -# add_subdirectory(SDL_image) -# -# # SDL_image target should be created by the SDL_image CMakeLists.txt -# endif() diff --git a/thirdparty/SDL b/thirdparty/SDL deleted file mode 160000 index ee5e249..0000000 --- a/thirdparty/SDL +++ /dev/null @@ -1 +0,0 @@ -Subproject commit ee5e249008b516c4efeb807fd8c9079c19732e53 diff --git a/thirdparty/SDL_image b/thirdparty/SDL_image deleted file mode 160000 index 9415941..0000000 --- a/thirdparty/SDL_image +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 9415941e14b633ff3cdea859cb296147f9bb7dfa diff --git a/thirdparty/assimp b/thirdparty/assimp deleted file mode 160000 index 26e2372..0000000 --- a/thirdparty/assimp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 26e2372882d681d996566360d3fc08cb43bced92 diff --git a/thirdparty/rapidjson b/thirdparty/rapidjson deleted file mode 160000 index fdc75bb..0000000 --- a/thirdparty/rapidjson +++ /dev/null @@ -1 +0,0 @@ -Subproject commit fdc75bb7746c79c1e1687dcc1a02ad2e0a6b146a