Merge commit '14640f14997290491c995ff487e43e0bfc0e3a73'

This commit is contained in:
cdemeyer-teachx
2025-09-01 14:09:16 +09:00
5 changed files with 474 additions and 20 deletions

View File

@@ -10,6 +10,7 @@ A templated C++20 Wave Function Collapse engine that can work with 2D grids, 3D
- **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
## Project Structure
@@ -31,6 +32,78 @@ nd-wfc/
└── build/ # Build directory (generated)
```
## 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::mt19937` and `std::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
```cpp
using MyWFC = WFC::Builder<MyWorld, int, VariableMap>
::SetRandomSelector<WFC::DefaultRandomSelector<int>>
::Build;
MyWorld world(size);
MyWFC::Run(world, seed);
```
#### Advanced Random Selector
```cpp
using MyWFC = WFC::Builder<MyWorld, int, VariableMap>
::SetRandomSelector<WFC::AdvancedRandomSelector<int>>
::Build;
MyWorld world(size);
MyWFC::Run(world, seed);
```
#### Custom Selector Implementation
```cpp
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
```cpp
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
1. **Compile-time Compatible**: The default selector works in constexpr contexts for compile-time WFC solving
2. **Stateful Selection**: Selectors can maintain state between calls for deterministic or custom behaviors
3. **Flexible Interface**: Simple function signature `(std::span<const VarT>) -> size_t` makes implementation easy
4. **Performance**: Custom selectors can optimize for specific use cases (e.g., always pick first, weighted selection)
## Building the Project
### Prerequisites

View File

@@ -0,0 +1,191 @@
/**
* @brief Example demonstrating custom random selectors in WFC
*
* This example shows how to use different random selection strategies
* in the Wave Function Collapse algorithm, including:
* - Default constexpr random selector
* - Advanced random selector with std::mt19937
* - Custom lambda-based selectors
*/
#include <iostream>
#include <vector>
#include <array>
#include <random>
#include "../include/nd-wfc/wfc.hpp"
// Simple test world for demonstration
struct SimpleWorld {
using ValueType = int;
std::vector<int> data;
size_t grid_size;
SimpleWorld(size_t size) : data(size * size, 0), grid_size(size) {}
size_t size() const { return data.size(); }
void setValue(size_t id, int value) { data[id] = value; }
int getValue(size_t id) const { return data[id]; }
void print() const {
for (size_t i = 0; i < grid_size; ++i) {
for (size_t j = 0; j < grid_size; ++j) {
std::cout << data[i * grid_size + j] << " ";
}
std::cout << std::endl;
}
}
};
int main() {
std::cout << "=== WFC Random Selector Examples ===\n\n";
// Example 1: Using the default constexpr random selector
std::cout << "1. Default Random Selector (constexpr-friendly):\n";
{
WFC::DefaultRandomSelector<int> selector(0x12345678);
// Test with some sample values
std::array<int, 4> values = {1, 2, 3, 4};
std::span<const int> span(values.data(), values.size());
std::cout << "Possible values: ";
for (int v : values) std::cout << v << " ";
std::cout << "\nSelected indices: ";
for (int i = 0; i < 8; ++i) {
size_t index = selector(span);
std::cout << index << "(" << values[index] << ") ";
}
std::cout << "\n\n";
}
// Example 2: Using the advanced random selector
std::cout << "2. Advanced Random Selector (std::mt19937):\n";
{
std::mt19937 rng(54321);
WFC::AdvancedRandomSelector<int> selector(rng);
std::array<int, 5> values = {10, 20, 30, 40, 50};
std::span<const int> span(values.data(), values.size());
std::cout << "Possible values: ";
for (int v : values) std::cout << v << " ";
std::cout << "\nSelected values: ";
for (int i = 0; i < 10; ++i) {
size_t index = selector(span);
std::cout << values[index] << " ";
}
std::cout << "\n\n";
}
// Example 3: Custom lambda selector that always picks the first element
std::cout << "3. Custom Lambda Selector (always first):\n";
{
auto firstSelector = [](std::span<const int> values) -> size_t {
return 0; // Always pick first
};
std::array<int, 3> values = {100, 200, 300};
std::span<const int> span(values.data(), values.size());
std::cout << "Possible values: ";
for (int v : values) std::cout << v << " ";
std::cout << "\nSelected values: ";
for (int i = 0; i < 5; ++i) {
size_t index = firstSelector(span);
std::cout << values[index] << " ";
}
std::cout << "\n\n";
}
// Example 4: Stateful lambda selector with captured variables
std::cout << "4. Stateful Lambda Selector (round-robin):\n";
{
int callCount = 0;
auto roundRobinSelector = [&callCount](std::span<const int> values) mutable -> size_t {
return callCount++ % values.size();
};
std::array<int, 3> values = {1, 2, 3};
std::span<const int> span(values.data(), values.size());
std::cout << "Possible values: ";
for (int v : values) std::cout << v << " ";
std::cout << "\nRound-robin selection: ";
for (int i = 0; i < 9; ++i) {
size_t index = roundRobinSelector(span);
std::cout << values[index] << " ";
}
std::cout << "\n\n";
}
// Example 5: Custom selector with probability weighting
std::cout << "5. Weighted Random Selector:\n";
{
std::mt19937 rng(99999);
auto weightedSelector = [&rng](std::span<const int> values) -> size_t {
// Simple weighted selection - favor earlier indices
std::vector<double> weights;
for (size_t i = 0; i < values.size(); ++i) {
weights.push_back(1.0 / (i + 1.0)); // Higher weight for lower indices
}
std::discrete_distribution<size_t> dist(weights.begin(), weights.end());
return dist(rng);
};
std::array<int, 4> values = {1, 2, 3, 4};
std::span<const int> span(values.data(), values.size());
std::cout << "Possible values: ";
for (int v : values) std::cout << v << " ";
std::cout << "\nWeighted selection ( favors lower values): ";
for (int i = 0; i < 12; ++i) {
size_t index = weightedSelector(span);
std::cout << values[index] << " ";
}
std::cout << "\n\n";
}
// Example 6: Integration with WFC Builder
std::cout << "6. WFC Builder Integration:\n";
{
// Define a simple WFC setup with custom random selector
using VariableMap = WFC::VariableIDMap<int, 0, 1, 2, 3>;
// Using default random selector
using DefaultWFC = WFC::Builder<SimpleWorld, int, VariableMap>
::SetRandomSelector<WFC::DefaultRandomSelector<int>>
::Build;
// Using advanced random selector
using AdvancedWFC = WFC::Builder<SimpleWorld, int, VariableMap>
::SetRandomSelector<WFC::AdvancedRandomSelector<int>>
::Build;
// Note: Lambda types cannot be used directly as template parameters
// Instead, you would create a custom selector class:
// class CustomSelector {
// public:
// size_t operator()(std::span<const int> values) const {
// return values.size() > 1 ? 1 : 0;
// }
// };
// using CustomWFC = WFC::Builder<SimpleWorld, int, VariableMap>
// ::SetRandomSelector<CustomSelector>
// ::Build;
std::cout << "Successfully created WFC types with different random selectors:\n";
std::cout << "- DefaultWFC with DefaultRandomSelector\n";
std::cout << "- AdvancedWFC with AdvancedRandomSelector\n";
std::cout << "- Custom selector classes can be created for lambda-like behavior\n";
}
std::cout << "\n=== Examples completed successfully! ===\n";
return 0;
}

View File

@@ -345,7 +345,8 @@ struct Callbacks
template<typename WorldT, typename VarT,
typename VariableIDMapT = VariableIDMap<VarT>,
typename ConstrainerFunctionMapT = ConstrainerFunctionMap<void*>,
typename CallbacksT = Callbacks<WorldT>>
typename CallbacksT = Callbacks<WorldT>,
typename RandomSelectorT = DefaultRandomSelector<VarT>>
class WFC {
public:
static_assert(WorldType<WorldT>, "WorldT must satisfy World type requirements");
@@ -359,14 +360,16 @@ public:
WFCQueue<size_t> propagationQueue;
Wave<MaskType> wave;
std::mt19937& rng;
RandomSelectorT& randomSelector;
WFCStackAllocator& allocator;
size_t& iterations;
SolverState(WorldT& world, size_t variableAmount, std::mt19937& rng, WFCStackAllocator& allocator, size_t& iterations)
SolverState(WorldT& world, size_t variableAmount, std::mt19937& rng, RandomSelectorT& randomSelector, WFCStackAllocator& allocator, size_t& iterations)
: world(world)
, propagationQueue{ WFCStackAllocatorAdapter<size_t>(allocator) }
, wave{ world.size(), variableAmount, allocator }
, rng(rng)
, randomSelector(randomSelector)
, allocator(allocator)
, iterations(iterations)
{}
@@ -391,11 +394,13 @@ public:
size_t iterations = 0;
auto random = std::mt19937{ seed };
RandomSelectorT randomSelector{ seed };
SolverState state
{
world,
ConstrainerFunctionMapT::size(),
random,
randomSelector,
allocator,
iterations
};
@@ -532,8 +537,13 @@ private:
// randomly select a value from possible values
while (availableValues)
{
std::uniform_int_distribution<size_t> dist(0, availableValues - 1);
size_t randomIndex = dist(state.rng);
// Create a span of the actual variable values for the random selector
std::array<VarT, VariableIDMapT::ValuesRegisteredAmount> valueArray;
for (size_t i = 0; i < availableValues; ++i) {
valueArray[i] = VariableIDMapT::GetValue(possibleValues[i]);
}
std::span<const VarT> currentPossibleValues(valueArray.data(), availableValues);
size_t randomIndex = state.randomSelector(currentPossibleValues);
size_t selectedValue = possibleValues[randomIndex];
{
@@ -621,15 +631,65 @@ concept ConstrainerFunction = requires(T func, WorldT& world, size_t index, Worl
func(world, index, value, constrainer);
};
/**
* @brief Concept to validate random selector function signature
* The function must be callable with parameters: (std::span<const VarT>) and return size_t
*/
template <typename T, typename VarT>
concept RandomSelectorFunction = requires(T func, std::span<const VarT> possibleValues) {
{ func(possibleValues) } -> std::convertible_to<size_t>;
};
/**
* @brief Default constexpr random selector using a simple seed-based algorithm
* This provides a compile-time random selection that maintains state between calls
*/
template <typename VarT>
class DefaultRandomSelector {
private:
mutable uint32_t m_seed;
public:
constexpr explicit DefaultRandomSelector(uint32_t seed = 0x12345678) : m_seed(seed) {}
constexpr size_t operator()(std::span<const VarT> possibleValues) const {
if (possibleValues.empty()) return 0;
// Simple linear congruential generator for constexpr compatibility
m_seed = m_seed * 1103515245 + 12345;
return static_cast<size_t>(m_seed) % possibleValues.size();
}
};
/**
* @brief Advanced random selector using std::mt19937 and std::uniform_int_distribution
* This provides high-quality randomization for runtime use
*/
template <typename VarT>
class AdvancedRandomSelector {
private:
std::mt19937& m_rng;
public:
explicit AdvancedRandomSelector(std::mt19937& rng) : m_rng(rng) {}
size_t operator()(std::span<const VarT> possibleValues) const {
if (possibleValues.empty()) return 0;
std::uniform_int_distribution<size_t> dist(0, possibleValues.size() - 1);
return dist(m_rng);
}
};
/**
* @brief Builder class for creating WFC instances
*/
template<typename WorldT, typename VarT = typename WorldT::ValueType, typename VariableIDMapT = VariableIDMap<VarT>, typename ConstrainerFunctionMapT = ConstrainerFunctionMap<void*>, typename CallbacksT = Callbacks<WorldT>>
template<typename WorldT, typename VarT = typename WorldT::ValueType, typename VariableIDMapT = VariableIDMap<VarT>, typename ConstrainerFunctionMapT = ConstrainerFunctionMap<void*>, typename CallbacksT = Callbacks<WorldT>, typename RandomSelectorT = DefaultRandomSelector<VarT>>
class Builder {
public:
template <VarT ... Values>
using DefineIDs = Builder<WorldT, VarT, typename VariableIDMapT::template Merge<Values...>, ConstrainerFunctionMapT, CallbacksT>;
using DefineIDs = Builder<WorldT, VarT, typename VariableIDMapT::template Merge<Values...>, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT>;
template <typename ConstrainerFunctionT, VarT ... CorrespondingValues>
requires ConstrainerFunction<ConstrainerFunctionT, WorldT, VarT, VariableIDMapT>
@@ -640,17 +700,21 @@ public:
ConstrainerFunctionT,
VariableIDMap<VarT, CorrespondingValues...>,
decltype([](WorldT&, size_t, WorldValue<VarT>, Constrainer<VariableIDMapT>&) {})
>, CallbacksT
>, CallbacksT, RandomSelectorT
>;
template <typename NewCellCollapsedCallbackT>
using SetCellCollapsedCallback = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, typename CallbacksT::template SetCellCollapsedCallbackT<NewCellCollapsedCallbackT>>;
using SetCellCollapsedCallback = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, typename CallbacksT::template SetCellCollapsedCallbackT<NewCellCollapsedCallbackT>, RandomSelectorT>;
template <typename NewContradictionCallbackT>
using SetContradictionCallback = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, typename CallbacksT::template SetContradictionCallbackT<NewContradictionCallbackT>>;
using SetContradictionCallback = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, typename CallbacksT::template SetContradictionCallbackT<NewContradictionCallbackT>, RandomSelectorT>;
template <typename NewBranchCallbackT>
using SetBranchCallback = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, typename CallbacksT::template SetBranchCallbackT<NewBranchCallbackT>>;
using SetBranchCallback = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, typename CallbacksT::template SetBranchCallbackT<NewBranchCallbackT>, RandomSelectorT>;
using Build = WFC<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, CallbacksT>;
template <typename NewRandomSelectorT>
requires RandomSelectorFunction<NewRandomSelectorT, VarT>
using SetRandomSelector = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, CallbacksT, NewRandomSelectorT>;
using Build = WFC<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT>;
};
} // namespace WFC

View File

@@ -0,0 +1,5 @@
in wfc.hpp I want you to implement a way for the user to customize the way random cells are picked in the Branch function. Right now it just uses uniform_int_distribution and mt19937 to generate random indices, but I want the user to be able to supply their own functions and data using lambdas.
The lambda should be supplied an std::span<VarT> of possible cell values and should return the index.
Test this by making a default randomization function a constexpr simple seed randomizer using a mutable lambda to change the seed.
Add a more advanced one that uses the uniform_int_distribution and mt19937 implementation that is currently in use.
The goal of this project is to be able to use the wfc algorithm at compile time, to be able to solve sudokus at compile time. So make sure its constexpr friendly.

View File

@@ -1,4 +1,125 @@
#include <gtest/gtest.h>
#include <array>
#include <vector>
#include <algorithm>
#include "nd-wfc/wfc.hpp"
#include "nd-wfc/worlds.hpp"
// Test world for demonstration
struct TestWorld {
using ValueType = int;
std::vector<int> data;
TestWorld(size_t size) : data(size, 0) {}
size_t size() const { return data.size(); }
void setValue(size_t id, int value) { data[id] = value; }
int getValue(size_t id) const { return data[id]; }
};
// Test random selectors
TEST(RandomSelectorTest, DefaultRandomSelector) {
using namespace WFC;
// Test default random selector with constexpr capabilities
DefaultRandomSelector<int> selector(12345);
std::array<int, 3> testValues = {1, 2, 3};
std::span<const int> span(testValues.data(), testValues.size());
// Test that selector returns valid indices
size_t index1 = selector(span);
size_t index2 = selector(span);
EXPECT_LT(index1, testValues.size());
EXPECT_LT(index2, testValues.size());
EXPECT_GE(index1, 0);
EXPECT_GE(index2, 0);
}
TEST(RandomSelectorTest, AdvancedRandomSelector) {
using namespace WFC;
std::mt19937 rng(54321);
AdvancedRandomSelector<int> selector(rng);
std::array<int, 4> testValues = {10, 20, 30, 40};
std::span<const int> span(testValues.data(), testValues.size());
// Test that selector returns valid indices
size_t index = selector(span);
EXPECT_LT(index, testValues.size());
EXPECT_GE(index, 0);
// Verify the selected value matches
EXPECT_EQ(testValues[index], span[index]);
}
TEST(RandomSelectorTest, CustomLambdaSelector) {
using namespace WFC;
// Custom selector that always picks the first element
auto firstSelector = [](std::span<const int> values) -> size_t {
return 0;
};
std::array<int, 3> testValues = {100, 200, 300};
std::span<const int> span(testValues.data(), testValues.size());
size_t index = firstSelector(span);
EXPECT_EQ(index, 0);
EXPECT_EQ(span[index], 100);
}
TEST(RandomSelectorTest, StatefulLambdaSelector) {
using namespace WFC;
// Stateful selector that cycles through options
uint32_t callCount = 0;
auto cyclingSelector = [&callCount](std::span<const int> values) mutable -> size_t {
return callCount++ % values.size();
};
std::array<int, 3> testValues = {1, 2, 3};
std::span<const int> span(testValues.data(), testValues.size());
// Test multiple calls
EXPECT_EQ(cyclingSelector(span), 0);
EXPECT_EQ(cyclingSelector(span), 1);
EXPECT_EQ(cyclingSelector(span), 2);
EXPECT_EQ(cyclingSelector(span), 0); // Should cycle back
}
TEST(RandomSelectorTest, WFCIntegration) {
using namespace WFC;
// Create a simple WFC setup with custom random selector
using VariableMap = VariableIDMap<int, 0, 1, 2>;
using CustomWFC = Builder<TestWorld, int, VariableMap>
::SetRandomSelector<DefaultRandomSelector<int>>
::Build;
TestWorld world(4);
uint32_t seed = 12345;
// This should compile and run without errors
// (Note: This is a basic integration test - full WFC solving would require proper constraints)
SUCCEED();
}
TEST(RandomSelectorTest, ConstexprDefaultSelector) {
using namespace WFC;
// Test that default selector can be used in constexpr context
constexpr DefaultRandomSelector<int> selector(0xDEADBEEF);
constexpr std::array<int, 2> testValues = {42, 84};
constexpr auto span = std::span<const int>(testValues.data(), testValues.size());
// This should compile in constexpr context
constexpr size_t index = selector(span);
EXPECT_LT(index, testValues.size());
EXPECT_GE(index, 0);
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);