9.5 KiB
N-Dimensional Wave Function Collapse (WFC) Library
A templated C++20 Wave Function Collapse engine that can work with 2D grids, 3D grids, Sudoku, and any other constraint satisfaction problem. The engine is coordinate-system agnostic and can work with any graph-like structure.
Features
- 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
- Custom Random Selectors: Pluggable random selection strategies for cell collapse, including compile-time compatible options
Custom Random Selectors
The WFC library now supports customizable random selection strategies for choosing cell values during the collapse process. This allows users to implement different randomization algorithms while maintaining compile-time compatibility.
Features
- Default Random Selector: A constexpr-compatible seed-based randomizer using linear congruential generator
- Advanced Random Selector: High-quality randomization using
std::mt19937andstd::uniform_int_distribution - Custom Selectors: Support for user-defined selector classes that can capture state and maintain behavior between calls
- Lambda Support: Selectors can be implemented as lambdas with captured variables for flexible behavior
Usage
Basic Usage with Default Selector
using MyWFC = WFC::Builder<MyWorld, int, VariableMap>
::SetRandomSelector<WFC::DefaultRandomSelector<int>>
::Build;
MyWorld world(size);
MyWFC::Run(world, seed);
Advanced Random Selector
using MyWFC = WFC::Builder<MyWorld, int, VariableMap>
::SetRandomSelector<WFC::AdvancedRandomSelector<int>>
::Build;
MyWorld world(size);
MyWFC::Run(world, seed);
Custom Selector Implementation
class CustomSelector {
private:
mutable int callCount = 0;
public:
size_t operator()(std::span<const int> possibleValues) const {
// Round-robin selection
return callCount++ % possibleValues.size();
}
};
using MyWFC = WFC::Builder<MyWorld, int, VariableMap>
::SetRandomSelector<CustomSelector>
::Build;
Stateful Lambda Selector
int counter = 0;
auto selector = [&counter](std::span<const int> values) mutable -> size_t {
return counter++ % values.size();
};
// Use with WFC (would need to wrap in a class for template parameter)
Key Benefits
- Compile-time Compatible: The default selector works in constexpr contexts for compile-time WFC solving
- Stateful Selection: Selectors can maintain state between calls for deterministic or custom behaviors
- Flexible Interface: Simple function signature
(std::span<const VarT>) -> size_tmakes implementation easy - Performance: Custom selectors can optimize for specific use cases (e.g., always pick first, weighted selection)
Building the Project
Prerequisites
- CMake 3.16 or higher
- C++20 compatible compiler (GCC 10+, Clang 10+, MSVC 2019+)
- Git
Build Instructions
# Clone the repository
git clone <repository-url>
cd nd-wfc
# Create build directory
mkdir build
cd build
# Configure with CMake
cmake ..
# Build the project
make
# Run basic test
./src/nd-wfc-main
# Run examples
./examples/tilemap_example
./examples/simple_sudoku_example
Usage
Basic Example (2x2 Grid)
#include <nd-wfc/wfc.h>
#include <iostream>
int main() {
enum SimpleTiles { A = 1, B = 2 };
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>());
if (wfc->run()) {
std::cout << "Solution found!" << std::endl;
// Print solution...
}
return 0;
}
2D Tilemap Example
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>());
World Types
The library provides several built-in World types:
Array2D<T, Width, Height>
2D array with compile-time dimensions.
WFC::Array2D<uint8_t, 100, 100> world;
Array3D<T, Width, Height, Depth>
3D array with compile-time dimensions.
WFC::Array3D<uint8_t, 10, 10, 10> world;
GraphWorld
Graph-based world for non-grid structures.
WFC::GraphWorld<uint8_t> world(100); // 100 nodes
world.addEdge(0, 1); // Connect nodes
SudokuWorld
Specialized 9x9 grid with Sudoku helper functions.
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:
struct MyWorld {
using ValueType = MyValueType;
using CoordType = MyCoordType;
size_t size() const;
int getId(CoordType coord) const;
CoordType getCoord(int id) const;
};
API Reference
Builder
Builder& variable(VarT value, ConstraintFunc func)- Add a variable with constraint functionstd::unique_ptr<WFC<WorldT>> build(WorldT world)- Build the WFC instance
WFC
bool run()- Run the WFC algorithmstd::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 valuesvoid only(int cellId, int value)- Constrain to single valuevoid 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/Landsudoku_example.cpp- Full 9x9 Sudoku solversimple_sudoku_example.cpp- Simple 2x2 Sudoku for testing
Running Examples
cd build/examples
./tilemap_example # Generate a 100x100 tilemap
./simple_sudoku_example # Solve a simple 2x2 Sudoku
Architecture
The WFC engine consists of:
- World Types: Define the problem space and coordinate systems
- Builder Pattern: Fluent interface for setting up variables and constraints
- Constraint System: Lambda-based constraint definitions
- Wave Function Collapse Algorithm: Core WFC implementation with observation and propagation
- Propagation Queue: Efficient constraint propagation system
Contributing
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests for new functionality
- Ensure all tests pass
- Submit a pull request
License
This project is licensed under the MIT License - see the LICENSE file for details.
References
- Original WFC Algorithm: https://github.com/mxgmn/WaveFunctionCollapse
- Constraint satisfaction and procedural generation concepts