wfc compiled

This commit is contained in:
cdemeyer-teachx
2025-08-24 17:14:46 +09:00
parent de5317967d
commit 76ad49b05a
31 changed files with 973 additions and 1785 deletions

1
.gitignore vendored
View File

@@ -6,7 +6,6 @@ bin/
lib/
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo

9
.gitmodules vendored
View File

@@ -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
View 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
View 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"
},
]
}

View File

@@ -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
View File

@@ -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

View File

@@ -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
View 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;
}

View 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

Binary file not shown.

BIN
demos/sudoku/sudoku_wfc_manual Executable file

Binary file not shown.

View File

@@ -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()

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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"

View File

@@ -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);
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;
}
// 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
}
}
// 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
View 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
View 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);
})
...

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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

Submodule thirdparty/SDL deleted from ee5e249008

Submodule thirdparty/SDL_image deleted from 9415941e14

1
thirdparty/assimp vendored

Submodule thirdparty/assimp deleted from 26e2372882

Submodule thirdparty/rapidjson deleted from fdc75bb774