wfc compiled
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,7 +6,6 @@ bin/
|
||||
lib/
|
||||
|
||||
# IDE and editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
9
.gitmodules
vendored
9
.gitmodules
vendored
@@ -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
|
||||
|
||||
23
.vscode/launch.json
vendored
Normal file
23
.vscode/launch.json
vendored
Normal file
@@ -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"
|
||||
]
|
||||
}
|
||||
21
.vscode/tasks.json
vendored
Normal file
21
.vscode/tasks.json
vendored
Normal file
@@ -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"
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
249
README.md
249
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 <nd-wfc/wfc.h>
|
||||
#include <iostream>
|
||||
|
||||
int main() {
|
||||
// Create a 10x10 grid
|
||||
nd_wfc::Size<2> size = {10, 10};
|
||||
enum SimpleTiles { A = 1, B = 2 };
|
||||
|
||||
// Define tile types
|
||||
std::vector<nd_wfc::TileId> tiles = {0, 1, 2, 3};
|
||||
auto wfc = WFC::Builder<WFC::Array2D<uint8_t, 2, 2>>()
|
||||
.variable(A, [](WFC::Array2D<uint8_t, 2, 2>& 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<uint8_t, 2, 2>& 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<uint8_t, 2, 2>());
|
||||
|
||||
// Define constraints (adjacency rules)
|
||||
std::vector<nd_wfc::Constraint> 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<WFC::Array2D<uint8_t, 100, 100>>()
|
||||
.variable(SEA, [](WFC::Array2D<uint8_t, 100, 100>& 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<uint8_t, 100, 100>& 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<uint8_t, 100, 100>& 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<uint8_t, 100, 100>());
|
||||
```
|
||||
|
||||
## 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<T, Width, Height>
|
||||
2D array with compile-time dimensions.
|
||||
|
||||
```cpp
|
||||
WFC::Array2D<uint8_t, 100, 100> world;
|
||||
```
|
||||
|
||||
### Array3D<T, Width, Height, Depth>
|
||||
3D array with compile-time dimensions.
|
||||
|
||||
```cpp
|
||||
WFC::Array3D<uint8_t, 10, 10, 10> world;
|
||||
```
|
||||
|
||||
### GraphWorld<T>
|
||||
Graph-based world for non-grid structures.
|
||||
|
||||
```cpp
|
||||
WFC::GraphWorld<uint8_t> 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<WorldT>
|
||||
|
||||
- `Builder& variable(VarT value, ConstraintFunc func)` - Add a variable with constraint function
|
||||
- `std::unique_ptr<WFC<WorldT>> build(WorldT world)` - Build the WFC instance
|
||||
|
||||
### WFC<WorldT>
|
||||
|
||||
- `bool run()` - Run the WFC algorithm
|
||||
- `std::optional<VarT> getValue(int cellId)` - Get value at cell (if collapsed)
|
||||
- `const std::unordered_set<int>& getPossibleValues(int cellId)` - Get possible values for cell
|
||||
|
||||
### Constrainer
|
||||
|
||||
- `void constrain(int cellId, const std::unordered_set<int>& 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
|
||||
|
||||
@@ -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()
|
||||
|
||||
19
demos/sudoku/main.cpp
Normal file
19
demos/sudoku/main.cpp
Normal file
@@ -0,0 +1,19 @@
|
||||
#include "sudoku.h"
|
||||
#include <iostream>
|
||||
|
||||
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;
|
||||
}
|
||||
58
demos/sudoku/sudoku_wfc.cpp
Normal file
58
demos/sudoku/sudoku_wfc.cpp
Normal file
@@ -0,0 +1,58 @@
|
||||
#include <nd-wfc/wfc.hpp>
|
||||
#include <nd-wfc/worlds.hpp>
|
||||
#include <iostream>
|
||||
|
||||
int main()
|
||||
{
|
||||
std::cout << "Running Sudoku WFC" << std::endl;
|
||||
|
||||
auto sudokuSolver = WFC::Builder<WFC::Array2D<uint8_t, 9, 9>, uint8_t>()
|
||||
.DefineIDs<1, 2, 3, 4, 5, 6, 7, 8, 9>()
|
||||
.Variable<1, 2, 3, 4, 5, 6, 7, 8, 9>([](WFC::Array2D<uint8_t, 9, 9>&, size_t index, WFC::WorldValue<uint8_t> 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<uint8_t, 9, 9> 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<int>(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;
|
||||
}
|
||||
}
|
||||
BIN
demos/sudoku/sudoku_wfc_demo
Executable file
BIN
demos/sudoku/sudoku_wfc_demo
Executable file
Binary file not shown.
BIN
demos/sudoku/sudoku_wfc_manual
Executable file
BIN
demos/sudoku/sudoku_wfc_manual
Executable file
Binary file not shown.
@@ -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()
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#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<nd_wfc::TileId> tiles = {0, 1, 2, 3}; // Empty, Grass, Water, Mountain
|
||||
|
||||
// Define adjacency constraints
|
||||
std::vector<nd_wfc::Constraint> 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<int>(result) << std::endl;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "types.hpp"
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
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<std::uint64_t, std::unordered_set<TileId>> adjacency_rules_;
|
||||
std::vector<TileId> tiles_;
|
||||
std::unordered_map<TileId, Index> tile_to_index_;
|
||||
|
||||
public:
|
||||
ConstraintManager(const std::vector<TileId>& tiles, const std::vector<Constraint>& 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<TileId> 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<TileId> 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<Index>(-1);
|
||||
}
|
||||
|
||||
// Get tile ID from index
|
||||
TileId getTileId(Index index) const {
|
||||
assert(index < tiles_.size());
|
||||
return tiles_[index];
|
||||
}
|
||||
|
||||
// Get all tiles
|
||||
const std::vector<TileId>& 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<std::uint64_t>(tile) << 8) | static_cast<std::uint64_t>(direction);
|
||||
}
|
||||
|
||||
// Unpack a key back to (tile, direction)
|
||||
void unpackKey(std::uint64_t key, TileId& tile, Direction& direction) const {
|
||||
tile = static_cast<TileId>(key >> 8);
|
||||
direction = static_cast<Direction>(key & 0xFF);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace nd_wfc
|
||||
@@ -1,174 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "types.hpp"
|
||||
#include "wave.hpp"
|
||||
#include <vector>
|
||||
#include <random>
|
||||
#include <algorithm>
|
||||
|
||||
namespace nd_wfc {
|
||||
|
||||
// Entropy calculator for efficient cell selection during WFC
|
||||
template<std::size_t N>
|
||||
class EntropyCalculator {
|
||||
private:
|
||||
const Wave<N>* wave_;
|
||||
mutable std::vector<Index> entropy_sorted_indices_;
|
||||
mutable bool cache_dirty_;
|
||||
|
||||
public:
|
||||
explicit EntropyCalculator(const Wave<N>* 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<N>& 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<N> 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<Index> candidates;
|
||||
for (Index idx : entropy_sorted_indices_) {
|
||||
Coord<N> 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<std::size_t> 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<Coord<N>> findMinEntropyCells() const {
|
||||
updateCache();
|
||||
|
||||
if (entropy_sorted_indices_.empty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Get the minimum entropy value
|
||||
Index first_index = entropy_sorted_indices_[0];
|
||||
Coord<N> first_coord = wave_->indexToCoord(first_index);
|
||||
std::uint8_t min_entropy = wave_->getEntropy(first_coord);
|
||||
|
||||
if (min_entropy == 0) {
|
||||
return {}; // Contradiction
|
||||
}
|
||||
|
||||
std::vector<Coord<N>> result;
|
||||
for (Index idx : entropy_sorted_indices_) {
|
||||
Coord<N> 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<N> 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<float>(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<N> 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<N> coord_a = wave_->indexToCoord(a);
|
||||
Coord<N> 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
|
||||
@@ -1,146 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "types.hpp"
|
||||
#include <vector>
|
||||
#include <cassert>
|
||||
|
||||
namespace nd_wfc {
|
||||
|
||||
// N-dimensional grid for storing tiles and managing coordinate transformations
|
||||
template<std::size_t N>
|
||||
class Grid {
|
||||
private:
|
||||
Size<N> size_;
|
||||
std::vector<TileId> data_;
|
||||
Index total_size_;
|
||||
std::array<Index, N> strides_;
|
||||
|
||||
public:
|
||||
explicit Grid(const Size<N>& 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<TileId>(-1));
|
||||
}
|
||||
|
||||
// Convert n-dimensional coordinate to linear index
|
||||
Index coordToIndex(const Coord<N>& 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<N> indexToCoord(Index index) const {
|
||||
assert(index < total_size_);
|
||||
Coord<N> 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<N>& 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<N>& coord, Direction dir, Coord<N>& neighbor) const {
|
||||
auto offset = getDirectionOffset<N>(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<Index>(-offset[i])) {
|
||||
return false; // Would underflow
|
||||
}
|
||||
neighbor[i] = coord[i] - static_cast<Index>(-offset[i]);
|
||||
} else {
|
||||
neighbor[i] = coord[i] + static_cast<Index>(offset[i]);
|
||||
if (neighbor[i] >= size_[i]) {
|
||||
return false; // Out of bounds
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Access tile at coordinate
|
||||
TileId& operator[](const Coord<N>& coord) {
|
||||
return data_[coordToIndex(coord)];
|
||||
}
|
||||
|
||||
const TileId& operator[](const Coord<N>& 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<N>& getSize() const { return size_; }
|
||||
Index getTotalSize() const { return total_size_; }
|
||||
|
||||
// Get all neighbor coordinates for a given position
|
||||
std::vector<std::pair<Coord<N>, Direction>> getNeighbors(const Coord<N>& coord) const {
|
||||
std::vector<std::pair<Coord<N>, Direction>> neighbors;
|
||||
auto valid_dirs = getValidDirections<N>();
|
||||
|
||||
for (Direction dir : valid_dirs) {
|
||||
Coord<N> 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<TileId>(-1));
|
||||
}
|
||||
|
||||
// Check if all cells are filled
|
||||
bool isComplete() const {
|
||||
for (const auto& tile : data_) {
|
||||
if (tile == static_cast<TileId>(-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
|
||||
@@ -1,185 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "types.hpp"
|
||||
#include "wave.hpp"
|
||||
#include "constraint.hpp"
|
||||
#include "grid.hpp"
|
||||
#include <queue>
|
||||
#include <stack>
|
||||
|
||||
namespace nd_wfc {
|
||||
|
||||
// Constraint propagator for efficient constraint satisfaction
|
||||
template<std::size_t N>
|
||||
class Propagator {
|
||||
private:
|
||||
Wave<N>* wave_;
|
||||
const ConstraintManager* constraints_;
|
||||
Grid<N> grid_;
|
||||
|
||||
// Stack for propagation (more cache-friendly than queue for this use case)
|
||||
mutable std::stack<Coord<N>> propagation_stack_;
|
||||
|
||||
public:
|
||||
Propagator(Wave<N>* wave, const ConstraintManager* constraints)
|
||||
: wave_(wave), constraints_(constraints), grid_(wave->getSize()) {}
|
||||
|
||||
// Propagate constraints after a cell collapse
|
||||
bool propagate(const Coord<N>& initial_coord) {
|
||||
propagation_stack_.push(initial_coord);
|
||||
|
||||
while (!propagation_stack_.empty()) {
|
||||
Coord<N> 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<N>& coord) {
|
||||
if (!wave_->isCollapsed(coord)) {
|
||||
return true; // Nothing to propagate from uncollapsed cell
|
||||
}
|
||||
|
||||
TileId collapsed_tile = wave_->getCollapsedTile(coord);
|
||||
auto valid_directions = getValidDirections<N>();
|
||||
|
||||
// Check each neighbor
|
||||
for (Direction dir : valid_directions) {
|
||||
Coord<N> 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<N>& 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<N> coord = wave_->indexToCoord(i);
|
||||
if (wave_->isCollapsed(coord)) {
|
||||
if (!propagateFromCell(coord)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Continue propagation until no more changes
|
||||
while (!propagation_stack_.empty()) {
|
||||
Coord<N> 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<N> coord = wave_->indexToCoord(i);
|
||||
|
||||
if (!wave_->isCollapsed(coord)) {
|
||||
continue; // Skip uncollapsed cells
|
||||
}
|
||||
|
||||
TileId tile = wave_->getCollapsedTile(coord);
|
||||
auto valid_directions = getValidDirections<N>();
|
||||
|
||||
// Check each neighbor
|
||||
for (Direction dir : valid_directions) {
|
||||
Coord<N> 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
|
||||
@@ -1,142 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include <bitset>
|
||||
|
||||
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<std::size_t N>
|
||||
using Coord = std::array<Index, N>;
|
||||
|
||||
template<std::size_t N>
|
||||
using Size = std::array<Index, N>;
|
||||
|
||||
// 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<std::size_t N>
|
||||
constexpr std::array<int, N> getDirectionOffset(Direction dir) {
|
||||
std::array<int, N> 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<std::size_t N>
|
||||
constexpr std::array<Direction, N * 2> 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<MAX_TILES>;
|
||||
|
||||
} // namespace nd_wfc
|
||||
@@ -1,276 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "types.hpp"
|
||||
#include "grid.hpp"
|
||||
#include <vector>
|
||||
#include <random>
|
||||
|
||||
namespace nd_wfc {
|
||||
|
||||
// Wave function for tracking possible states at each grid cell
|
||||
template<std::size_t N>
|
||||
class Wave {
|
||||
private:
|
||||
Size<N> size_;
|
||||
Index total_size_;
|
||||
std::vector<TileMask> possibilities_;
|
||||
std::vector<std::uint8_t> entropy_cache_;
|
||||
std::vector<bool> is_collapsed_;
|
||||
std::vector<TileId> tiles_;
|
||||
Index num_tiles_;
|
||||
bool entropy_dirty_;
|
||||
|
||||
public:
|
||||
Wave(const Size<N>& size, const std::vector<TileId>& 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<std::uint8_t>(num_tiles_));
|
||||
is_collapsed_.resize(total_size_, false);
|
||||
}
|
||||
|
||||
// Convert coordinate to linear index
|
||||
Index coordToIndex(const Coord<N>& 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<N> indexToCoord(Index index) const {
|
||||
assert(index < total_size_);
|
||||
Coord<N> 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<N>& 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<N>& 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<N>& 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<N>& 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<N>& coord) const {
|
||||
Index cell_index = coordToIndex(coord);
|
||||
if (entropy_dirty_) {
|
||||
// Recalculate entropy cache
|
||||
const_cast<Wave*>(this)->updateEntropyCache();
|
||||
}
|
||||
return entropy_cache_[cell_index];
|
||||
}
|
||||
|
||||
// Check if a cell is collapsed
|
||||
bool isCollapsed(const Coord<N>& coord) const {
|
||||
return is_collapsed_[coordToIndex(coord)];
|
||||
}
|
||||
|
||||
// Get the collapsed tile at a coordinate (only valid if collapsed)
|
||||
TileId getCollapsedTile(const Coord<N>& 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<TileId>(-1);
|
||||
}
|
||||
|
||||
// Find the cell with minimum entropy (excluding collapsed cells)
|
||||
bool findMinEntropyCell(Coord<N>& coord, std::mt19937& rng) const {
|
||||
if (entropy_dirty_) {
|
||||
const_cast<Wave*>(this)->updateEntropyCache();
|
||||
}
|
||||
|
||||
std::uint8_t min_entropy = 255;
|
||||
std::vector<Index> 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<std::size_t> 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<N>& coord, std::mt19937& rng) const {
|
||||
Index cell_index = coordToIndex(coord);
|
||||
const auto& mask = possibilities_[cell_index];
|
||||
|
||||
std::vector<TileId> 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<TileId>(-1); // No possibilities
|
||||
}
|
||||
|
||||
std::uniform_int_distribution<std::size_t> 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<N>& 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<std::uint8_t>(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<Index>(-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<std::uint8_t>(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
|
||||
12
include/nd-wfc/wfc.h
Normal file
12
include/nd-wfc/wfc.h
Normal file
@@ -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"
|
||||
|
||||
@@ -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 <vector>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <queue>
|
||||
#include <random>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <concepts>
|
||||
|
||||
namespace nd_wfc {
|
||||
namespace WFC {
|
||||
|
||||
// Main Wave Function Collapse algorithm implementation
|
||||
template<std::size_t N>
|
||||
class Wfc {
|
||||
private:
|
||||
Size<N> size_;
|
||||
std::vector<TileId> tiles_;
|
||||
ConstraintManager constraints_;
|
||||
WfcConfig config_;
|
||||
|
||||
// Algorithm state
|
||||
Wave<N> wave_;
|
||||
Grid<N> result_grid_;
|
||||
EntropyCalculator<N> entropy_calc_;
|
||||
Propagator<N> propagator_;
|
||||
|
||||
// Runtime state
|
||||
std::mt19937 rng_;
|
||||
Index iteration_count_;
|
||||
WfcResult last_result_;
|
||||
|
||||
public:
|
||||
Wfc(const Size<N>& size, const std::vector<TileId>& tiles,
|
||||
const std::vector<Constraint>& 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<std::chrono::nanoseconds>(duration).count();
|
||||
rng_.seed(static_cast<std::uint32_t>(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<size_t>(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<N> 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<TileId>(-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<N>& 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<N>::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<N>& 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<N> coord = wave_.indexToCoord(i);
|
||||
if (wave_.isCollapsed(coord)) {
|
||||
result_grid_[coord] = wave_.getCollapsedTile(coord);
|
||||
} else {
|
||||
result_grid_[coord] = static_cast<TileId>(-1); // Should not happen on success
|
||||
}
|
||||
}
|
||||
}
|
||||
template<typename T>
|
||||
concept WorldType = requires(T world, size_t id, typename T::ValueType value) {
|
||||
{ world.size() } -> std::convertible_to<size_t>;
|
||||
{ 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 <typename MaskType>
|
||||
class Wave;
|
||||
template <typename VariableIDMapT>
|
||||
class Constrainer;
|
||||
template<typename WorldT, typename VarT, typename VariableIDMapT>
|
||||
class WFC;
|
||||
template<typename WorldT, typename VarT, typename VariableIDMapT>
|
||||
class Variable;
|
||||
template <typename VarT>
|
||||
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 <typename VarT, VarT ... Values>
|
||||
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 <VarT ... AdditionalValues>
|
||||
using Merge = VariableIDMap<VarT, Values..., AdditionalValues...>;
|
||||
|
||||
template <VarT Value>
|
||||
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 <VarT Value>
|
||||
static consteval size_t GetIndex()
|
||||
{
|
||||
static_assert(HasValue<Value>(), "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<size_t>(-1); // This line is unreachable if value is found
|
||||
}
|
||||
|
||||
static VarT GetValue(size_t index) {
|
||||
constexpr VarT arr[] = {Values...};
|
||||
return arr[index];
|
||||
}
|
||||
|
||||
template <VarT ... MaskValues>
|
||||
static consteval MaskType GetMask()
|
||||
{
|
||||
return (0 | ... | (1 << GetIndex<MaskValues>()));
|
||||
}
|
||||
|
||||
static consteval size_t size() { return sizeof...(Values); }
|
||||
};
|
||||
|
||||
template <typename VarT>
|
||||
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 <typename MaskType>
|
||||
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<uint16_t>(std::countr_zero(m_data[index])); }
|
||||
MaskType GetMask(size_t index) const { return m_data[index]; }
|
||||
|
||||
private:
|
||||
std::vector<MaskType> m_data;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Constrainer class used in constraint functions to limit possible values for other cells
|
||||
*/
|
||||
template <typename VariableIDMapT>
|
||||
class Constrainer {
|
||||
public:
|
||||
using MaskType = typename VariableIDMapT::MaskType;
|
||||
|
||||
public:
|
||||
Constrainer(Wave<MaskType>& wave, std::queue<size_t>& 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 <typename VariableIDMapT::Type ... ExcludedValues>
|
||||
void Exclude(size_t cellId) {
|
||||
static_assert(sizeof...(ExcludedValues) > 0, "At least one excluded value must be provided");
|
||||
ApplyMask(cellId, ~VariableIDMapT::template GetMask<ExcludedValues...>());
|
||||
}
|
||||
|
||||
void Exclude(WorldValue<typename VariableIDMapT::Type> 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 <typename VariableIDMapT::Type ... AllowedValues>
|
||||
void Only(size_t cellId) {
|
||||
static_assert(sizeof...(AllowedValues) > 0, "At least one allowed value must be provided");
|
||||
ApplyMask(cellId, VariableIDMapT::template GetMask<AllowedValues...>());
|
||||
}
|
||||
|
||||
void Only(WorldValue<typename VariableIDMapT::Type> 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<MaskType>& m_wave;
|
||||
std::queue<size_t>& m_propagationQueue;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Variable definition with its constraint function
|
||||
*/
|
||||
template<typename WorldT, typename VarT, typename VariableIDMapT>
|
||||
struct VariableData {
|
||||
VarT value{};
|
||||
std::function<void(WorldT&, size_t, WorldValue<VarT>, Constrainer<VariableIDMapT>&)> constraintFunc{};
|
||||
|
||||
VariableData() = default;
|
||||
VariableData(VarT value, std::function<void(WorldT&, size_t, WorldValue<VarT>, Constrainer<VariableIDMapT>&)> constraintFunc)
|
||||
: value(value)
|
||||
, constraintFunc(constraintFunc)
|
||||
{}
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Main WFC class implementing the Wave Function Collapse algorithm
|
||||
*/
|
||||
template<typename WorldT, typename VarT, typename VariableIDMapT = VariableIDMap<VarT>>
|
||||
class WFC {
|
||||
public:
|
||||
static_assert(WorldType<WorldT>, "WorldT must satisfy World type requirements");
|
||||
|
||||
using MaskType = typename VariableIDMapT::MaskType;
|
||||
|
||||
public:
|
||||
struct WorldSolver {
|
||||
WorldT& world;
|
||||
std::queue<size_t> propagationQueue;
|
||||
Wave<MaskType> wave;
|
||||
std::mt19937 rng;
|
||||
|
||||
WorldSolver(WorldT& world, const std::vector<VariableData<WorldT, VarT, VariableIDMapT>>& variables)
|
||||
: world(world)
|
||||
, propagationQueue()
|
||||
, wave(world.size(), variables.size())
|
||||
, rng(std::random_device{}())
|
||||
{}
|
||||
};
|
||||
|
||||
public:
|
||||
WFC(std::vector<VariableData<WorldT, VarT, VariableIDMapT>>&& 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<VarT> 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<VarT> GetPossibleValues(WorldSolver& worldSolver, int cellId) const
|
||||
{
|
||||
std::vector<VarT> 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<size_t>(-1);
|
||||
size_t minEntropy = static_cast<size_t>(-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<size_t> 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<VariableIDMapT> constrainer(worldSolver.wave, worldSolver.propagationQueue);
|
||||
m_variables[variableID].constraintFunc(worldSolver.world, cellId, WorldValue<VarT>{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<VariableData<WorldT, VarT, VariableIDMapT>> m_variables {};
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief Builder class for creating WFC instances
|
||||
*/
|
||||
template<typename WorldT, typename VarT, typename VariableIDMapT = VariableIDMap<VarT>>
|
||||
class Builder {
|
||||
public:
|
||||
Builder() = default;
|
||||
Builder(std::vector<VariableData<WorldT, VarT, VariableIDMapT>>&& variables)
|
||||
: m_variables(std::move(variables))
|
||||
{}
|
||||
|
||||
public:
|
||||
template <VarT ... Values>
|
||||
auto DefineIDs()
|
||||
{
|
||||
using NewVariableIDMapT = typename VariableIDMapT::template Merge<Values...>;
|
||||
// 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<WorldT, VarT, NewVariableIDMapT>(std::move(reinterpret_cast<std::vector<VariableData<WorldT, VarT, NewVariableIDMapT>>&>(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 <VarT ... Values>
|
||||
Builder& Variable(const std::function<void(WorldT&, size_t, WorldValue<VarT>, Constrainer<VariableIDMapT>&)> constraintFunc) {
|
||||
m_variables.resize(VariableIDMapT::ValuesRegisteredAmount);
|
||||
|
||||
Variable_Internal<Values...>(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<WorldT, VarT, VariableIDMapT>(std::move(m_variables));
|
||||
}
|
||||
|
||||
private:
|
||||
template <VarT Value, VarT ... Values>
|
||||
void Variable_Internal(const std::function<void(WorldT&, size_t, WorldValue<VarT>, Constrainer<VariableIDMapT>&)> constraintFunc)
|
||||
{
|
||||
m_variables[VariableIDMapT::template GetIndex<Value>()] = VariableData<WorldT, VarT, VariableIDMapT>{
|
||||
Value,
|
||||
constraintFunc
|
||||
};
|
||||
if constexpr (sizeof...(Values) > 0) {
|
||||
Variable_Internal<Values...>(constraintFunc);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<VariableData<WorldT, VarT, VariableIDMapT>> m_variables;
|
||||
};
|
||||
|
||||
} // namespace WFC
|
||||
|
||||
169
include/nd-wfc/worlds.hpp
Normal file
169
include/nd-wfc/worlds.hpp
Normal file
@@ -0,0 +1,169 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
namespace WFC {
|
||||
|
||||
/**
|
||||
* @brief 2D Array World implementation
|
||||
*/
|
||||
template<typename T, size_t Width, size_t Height>
|
||||
class Array2D {
|
||||
public:
|
||||
using ValueType = T;
|
||||
using CoordType = std::tuple<int, int>;
|
||||
|
||||
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<T, Width * Height> data_;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 3D Array World implementation
|
||||
*/
|
||||
template<typename T, size_t Width, size_t Height, size_t Depth>
|
||||
class Array3D {
|
||||
public:
|
||||
using ValueType = T;
|
||||
using CoordType = std::tuple<int, int, int>;
|
||||
|
||||
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<T, Width * Height * Depth> data_;
|
||||
};
|
||||
|
||||
} // namespace WFC
|
||||
|
||||
46
prompts/4-abstract-wfc
Normal file
46
prompts/4-abstract-wfc
Normal file
@@ -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<World>()
|
||||
.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<Array2D<uint8_t,9,9>>()
|
||||
.variable(1,2,3,4,5,6,7,8,9).constrains([](Array2D<uint8_t,9,9>& 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<Array2D<uint8_t, 100,100>>()
|
||||
.variable(SEA).constrains([](Array2D<uint8_t, 100,100>& 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<uint8_t, 100,100>& 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);
|
||||
})
|
||||
...
|
||||
244
src/main.cpp
244
src/main.cpp
@@ -1,248 +1,6 @@
|
||||
|
||||
|
||||
#include <nd-wfc/wfc.h>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
#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<nd_wfc::TileId> tiles = {0, 1, 2, 3};
|
||||
|
||||
// Define adjacency constraints for terrain generation
|
||||
std::vector<nd_wfc::Constraint> 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<std::chrono::microseconds>(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<int>(stats.min_entropy)
|
||||
<< ", Max: " << static_cast<int>(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<nd_wfc::TileId> tiles = {0, 1};
|
||||
|
||||
// Constraints: create a checkerboard-like pattern
|
||||
std::vector<nd_wfc::Constraint> 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<std::chrono::microseconds>(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<int>(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<nd_wfc::TileId> tiles = {0, 1, 2, 3};
|
||||
|
||||
// Same terrain constraints as before
|
||||
std::vector<nd_wfc::Constraint> 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<std::chrono::milliseconds>(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<int>(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;
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
#include <gtest/gtest.h>
|
||||
#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<TileId> 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<Constraint> 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
|
||||
80
thirdparty/CMakeLists.txt
vendored
80
thirdparty/CMakeLists.txt
vendored
@@ -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()
|
||||
1
thirdparty/SDL
vendored
1
thirdparty/SDL
vendored
Submodule thirdparty/SDL deleted from ee5e249008
1
thirdparty/SDL_image
vendored
1
thirdparty/SDL_image
vendored
Submodule thirdparty/SDL_image deleted from 9415941e14
1
thirdparty/assimp
vendored
1
thirdparty/assimp
vendored
Submodule thirdparty/assimp deleted from 26e2372882
1
thirdparty/rapidjson
vendored
1
thirdparty/rapidjson
vendored
Submodule thirdparty/rapidjson deleted from fdc75bb774
Reference in New Issue
Block a user