diff --git a/README.md b/README.md index 3947499..030da8c 100644 --- a/README.md +++ b/README.md @@ -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 + ::SetRandomSelector> + ::Build; + +MyWorld world(size); +MyWFC::Run(world, seed); +``` + +#### Advanced Random Selector + +```cpp +using MyWFC = WFC::Builder + ::SetRandomSelector> + ::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 possibleValues) const { + // Round-robin selection + return callCount++ % possibleValues.size(); + } +}; + +using MyWFC = WFC::Builder + ::SetRandomSelector + ::Build; +``` + +#### Stateful Lambda Selector + +```cpp +int counter = 0; +auto selector = [&counter](std::span 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) -> 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 diff --git a/demos/random_selector_example.cpp b/demos/random_selector_example.cpp new file mode 100644 index 0000000..0034481 --- /dev/null +++ b/demos/random_selector_example.cpp @@ -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 +#include +#include +#include + +#include "../include/nd-wfc/wfc.hpp" + +// Simple test world for demonstration +struct SimpleWorld { + using ValueType = int; + std::vector 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 selector(0x12345678); + + // Test with some sample values + std::array values = {1, 2, 3, 4}; + std::span 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 selector(rng); + + std::array values = {10, 20, 30, 40, 50}; + std::span 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 values) -> size_t { + return 0; // Always pick first + }; + + std::array values = {100, 200, 300}; + std::span 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 values) mutable -> size_t { + return callCount++ % values.size(); + }; + + std::array values = {1, 2, 3}; + std::span 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 values) -> size_t { + // Simple weighted selection - favor earlier indices + std::vector 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 dist(weights.begin(), weights.end()); + return dist(rng); + }; + + std::array values = {1, 2, 3, 4}; + std::span 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; + + // Using default random selector + using DefaultWFC = WFC::Builder + ::SetRandomSelector> + ::Build; + + // Using advanced random selector + using AdvancedWFC = WFC::Builder + ::SetRandomSelector> + ::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 values) const { + // return values.size() > 1 ? 1 : 0; + // } + // }; + // using CustomWFC = WFC::Builder + // ::SetRandomSelector + // ::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; +} diff --git a/include/nd-wfc/wfc.hpp b/include/nd-wfc/wfc.hpp index f90042b..7c2c958 100644 --- a/include/nd-wfc/wfc.hpp +++ b/include/nd-wfc/wfc.hpp @@ -342,10 +342,11 @@ struct Callbacks /** * @brief Main WFC class implementing the Wave Function Collapse algorithm */ -template, - typename ConstrainerFunctionMapT = ConstrainerFunctionMap, - typename CallbacksT = Callbacks> +template, + typename ConstrainerFunctionMapT = ConstrainerFunctionMap, + typename CallbacksT = Callbacks, + typename RandomSelectorT = DefaultRandomSelector> class WFC { public: static_assert(WorldType, "WorldT must satisfy World type requirements"); @@ -353,20 +354,22 @@ public: using MaskType = typename VariableIDMapT::MaskType; public: - struct SolverState + struct SolverState { WorldT& world; WFCQueue propagationQueue; Wave 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(allocator) } , wave{ world.size(), variableAmount, allocator } , rng(rng) + , randomSelector(randomSelector) , allocator(allocator) , iterations(iterations) {} @@ -388,14 +391,16 @@ public: { allocator.reset(); constexpr_assert(allocator.getUsed() == 0, "Allocator must be empty"); - + 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 dist(0, availableValues - 1); - size_t randomIndex = dist(state.rng); + // Create a span of the actual variable values for the random selector + std::array valueArray; + for (size_t i = 0; i < availableValues; ++i) { + valueArray[i] = VariableIDMapT::GetValue(possibleValues[i]); + } + std::span 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) and return size_t + */ +template +concept RandomSelectorFunction = requires(T func, std::span possibleValues) { + { func(possibleValues) } -> std::convertible_to; +}; + +/** + * @brief Default constexpr random selector using a simple seed-based algorithm + * This provides a compile-time random selection that maintains state between calls + */ +template +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 possibleValues) const { + if (possibleValues.empty()) return 0; + + // Simple linear congruential generator for constexpr compatibility + m_seed = m_seed * 1103515245 + 12345; + return static_cast(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 +class AdvancedRandomSelector { +private: + std::mt19937& m_rng; + +public: + explicit AdvancedRandomSelector(std::mt19937& rng) : m_rng(rng) {} + + size_t operator()(std::span possibleValues) const { + if (possibleValues.empty()) return 0; + + std::uniform_int_distribution dist(0, possibleValues.size() - 1); + return dist(m_rng); + } +}; + /** * @brief Builder class for creating WFC instances */ -template, typename ConstrainerFunctionMapT = ConstrainerFunctionMap, typename CallbacksT = Callbacks> +template, typename ConstrainerFunctionMapT = ConstrainerFunctionMap, typename CallbacksT = Callbacks, typename RandomSelectorT = DefaultRandomSelector> class Builder { public: template - using DefineIDs = Builder, ConstrainerFunctionMapT, CallbacksT>; + using DefineIDs = Builder, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT>; template requires ConstrainerFunction @@ -640,17 +700,21 @@ public: ConstrainerFunctionT, VariableIDMap, decltype([](WorldT&, size_t, WorldValue, Constrainer&) {}) - >, CallbacksT + >, CallbacksT, RandomSelectorT >; - - template - using SetCellCollapsedCallback = Builder>; - template - using SetContradictionCallback = Builder>; - template - using SetBranchCallback = Builder>; - using Build = WFC; + template + using SetCellCollapsedCallback = Builder, RandomSelectorT>; + template + using SetContradictionCallback = Builder, RandomSelectorT>; + template + using SetBranchCallback = Builder, RandomSelectorT>; + + template + requires RandomSelectorFunction + using SetRandomSelector = Builder; + + using Build = WFC; }; } // namespace WFC diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 76f841f..46b5285 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -1,4 +1,125 @@ #include +#include +#include +#include +#include "nd-wfc/wfc.hpp" +#include "nd-wfc/worlds.hpp" + +// Test world for demonstration +struct TestWorld { + using ValueType = int; + std::vector 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 selector(12345); + + std::array testValues = {1, 2, 3}; + std::span 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 selector(rng); + + std::array testValues = {10, 20, 30, 40}; + std::span 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 values) -> size_t { + return 0; + }; + + std::array testValues = {100, 200, 300}; + std::span 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 values) mutable -> size_t { + return callCount++ % values.size(); + }; + + std::array testValues = {1, 2, 3}; + std::span 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; + using CustomWFC = Builder + ::SetRandomSelector> + ::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 selector(0xDEADBEEF); + + constexpr std::array testValues = {42, 84}; + constexpr auto span = std::span(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);