Compare commits

...

5 Commits

Author SHA1 Message Date
Connor
f88f4c16a0 random generator bugfix 2026-02-09 23:54:49 +09:00
Connor
8567d86cee dungeon generator v0.1 2026-02-09 22:59:47 +09:00
Connor
480e1d5364 2d dungeon 2026-02-09 22:22:35 +09:00
Connor
b84970eddd initial state 2026-02-09 22:22:26 +09:00
Connor
414ded7e09 WFC class refactor 2026-02-06 20:05:16 +09:00
15 changed files with 607 additions and 420 deletions

View File

@@ -0,0 +1,46 @@
cmake_minimum_required(VERSION 3.16)
project(2d_dungeon_demo CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Add the main project as a subdirectory to get the nd-wfc library
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/../../src ${CMAKE_CURRENT_BINARY_DIR}/nd-wfc)
# Add compiler warnings
if(MSVC)
add_compile_options(/W4)
else()
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# Create demo executable
add_executable(dungeon_demo main.cpp)
# Link to nd-wfc library
target_link_libraries(dungeon_demo PRIVATE nd-wfc)
# Include directories
target_include_directories(dungeon_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# Set output directory and properties
set_target_properties(dungeon_demo PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON
)
# Optional: Enable optimizations for release builds
if(CMAKE_BUILD_TYPE STREQUAL "Release")
if(MSVC)
target_compile_options(dungeon_demo PRIVATE /O2)
else()
target_compile_options(dungeon_demo PRIVATE -O3 -march=native)
endif()
endif()
# Ensure consistent runtime library settings for MSVC
if(MSVC)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>DLL")
target_compile_options(dungeon_demo PRIVATE $<$<CONFIG:Debug>:/MDd> $<$<CONFIG:Release>:/MD>)
endif()

18
demos/2DDungeon/Makefile Normal file
View File

@@ -0,0 +1,18 @@
BUILD_DIR := build
BUILD_TYPE ?= Debug
.PHONY: all clean run configure build
all: build
configure:
cmake -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE)
build: configure
cmake --build $(BUILD_DIR)
run: build
./$(BUILD_DIR)/bin/dungeon_demo
clean:
rm -rf $(BUILD_DIR)

89
demos/2DDungeon/main.cpp Normal file
View File

@@ -0,0 +1,89 @@
#include <nd-wfc/wfc.hpp>
#include <nd-wfc/wfc_builder.hpp>
#include <nd-wfc/worlds.hpp>
#include <iostream>
// Tile types for the dungeon
// Values start at 1 so default-initialized cells (0) don't match any tile
enum class Tile : int {
Empty = 1,
Wall = 2,
Floor = 3
};
constexpr size_t DungeonWidth = 16;
constexpr size_t DungeonHeight = 16;
using World = WFC::Array2D<Tile, DungeonWidth, DungeonHeight>;
// Concrete types needed for constrainer lambda signatures
using IDMap = WFC::VariableIDMap<Tile, Tile::Floor, Tile::Wall, Tile::Empty>;
constexpr size_t WorldSize = DungeonWidth * DungeonHeight;
using WaveT = WFC::Wave<IDMap, WorldSize>;
using QueueT = WFC::WFCQueue<WorldSize, size_t>;
using ConstrainerT = WFC::Constrainer<WaveT, QueueT>;
void printDungeon(const World& world) {
for (size_t y = 0; y < DungeonHeight; ++y) {
for (size_t x = 0; x < DungeonWidth; ++x) {
switch (world.at(x, y)) {
case Tile::Floor: std::cout << '.'; break;
case Tile::Wall: std::cout << '#'; break;
case Tile::Empty: std::cout << ' '; break;
}
}
std::cout << '\n';
}
}
int main() {
std::cout << "2D Dungeon WFC Demo\n";
std::cout << "Dungeon size: " << DungeonWidth << "x" << DungeonHeight << "\n\n";
using DungeonBuilder = WFC::Builder<World>
::DefineIDs<Tile::Floor, Tile::Wall, Tile::Empty>
::Variable<Tile::Floor>
::Constrain<decltype([](World& world, size_t index, WFC::WorldValue<Tile> val, ConstrainerT& constrainer) {
auto [x, y] = world.getCoord(index);
// enable walls in 3x3 area around floor (without center)
// must come before Exclude to avoid collapsing then un-collapsing cells
for (int dy = -1; dy <= 1; ++dy) {
for (int dx = -1; dx <= 1; ++dx) {
if (dx == 0 && dy == 0) continue;
constrainer.template Include<Tile::Wall>(world.getCoordOffset(x, y, dx, dy));
}
}
// floor cannot be adjacent to empty space
constrainer.template Exclude<Tile::Empty>(world.getCoordOffset(x, y, -1, 0)); // Left
constrainer.template Exclude<Tile::Empty>(world.getCoordOffset(x, y, 1, 0)); // Right
constrainer.template Exclude<Tile::Empty>(world.getCoordOffset(x, y, 0, -1)); // Up
constrainer.template Exclude<Tile::Empty>(world.getCoordOffset(x, y, 0, 1)); // Down
})>
::SetInitialState<decltype([](World& world, auto& constrainer, auto&) constexpr {
// disable walls everywhere by default
for (size_t i = 0; i < world.size(); ++i) {
constrainer.template Exclude<Tile::Wall>(i);
}
// make it impossible for the edge to be floor
for (size_t x = 0; x < world.width(); ++x) {
constrainer.template Exclude<Tile::Floor>(world.getId({static_cast<int>(x), 0}));
constrainer.template Exclude<Tile::Floor>(world.getId({static_cast<int>(x), static_cast<int>(world.height() - 1)}));
}
// seed floor tiles to kick-start dungeon generation
constrainer.template Only<Tile::Floor>(world.getId({2, 2}));
})>
::SetRandomSelector<WFC::AdvancedRandomSelector<Tile>>
::Build;
World world{};
bool success = WFC::Run<DungeonBuilder>(world, std::random_device{}());
if (!success) {
std::cout << "WFC solver failed!\n";
}
printDungeon(world);
return 0;
}

32
demos/sudoku/Makefile Normal file
View File

@@ -0,0 +1,32 @@
BUILD_DIR := build
BUILD_TYPE ?= Release
.PHONY: all clean run test benchmark configure build
all: build
configure:
cmake -B $(BUILD_DIR) -DCMAKE_BUILD_TYPE=$(BUILD_TYPE)
build: configure
cmake --build $(BUILD_DIR) -j$(shell nproc)
run: build
./$(BUILD_DIR)/bin/sudoku_wfc_demo
test: build
@if [ -f ./$(BUILD_DIR)/bin/sudoku_tests ]; then \
./$(BUILD_DIR)/bin/sudoku_tests; \
else \
echo "Tests not available (Google Test not found)"; \
fi
benchmark: build
@if [ -f ./$(BUILD_DIR)/bin/sudoku_benchmarks ]; then \
./$(BUILD_DIR)/bin/sudoku_benchmarks; \
else \
echo "Benchmarks not available (Google Benchmark not found)"; \
fi
clean:
rm -rf $(BUILD_DIR)

View File

@@ -1,93 +0,0 @@
#!/bin/bash
# Simple build script for the Sudoku demo
set -e # Exit on any error
echo "Building Fast Sudoku Loader and Solution Validator..."
echo "======================================================"
# Create build directory if it doesn't exist
if [ ! -d "build" ]; then
echo "Creating build directory..."
mkdir build
fi
cd build
# Configure with CMake
echo "Configuring with CMake..."
cmake .. -DCMAKE_BUILD_TYPE=Release
# Build the project
echo "Building project..."
make -j$(nproc)
echo ""
echo "Build completed successfully!"
echo "Run the demo with: ./bin/sudoku_demo"
# Check what executables were built
available_executables=""
if [ -f "./bin/sudoku_tests" ]; then
echo "Run tests with: ./bin/sudoku_tests"
available_executables="$available_executables test"
fi
if [ -f "./bin/sudoku_benchmarks" ]; then
echo "Run benchmarks with: ./bin/sudoku_benchmarks"
available_executables="$available_executables benchmark"
fi
echo ""
# Handle different run modes
case "$1" in
"run")
echo "Running demo..."
./bin/sudoku_demo
;;
"test")
if [ -f "./bin/sudoku_tests" ]; then
echo "Running tests..."
./bin/sudoku_tests
else
echo "Tests not available (Google Test not found)"
fi
;;
"benchmark")
if [ -f "./bin/sudoku_benchmarks" ]; then
echo "Running benchmarks..."
./bin/sudoku_benchmarks
else
echo "Benchmarks not available (Google Benchmark not found)"
fi
;;
"all")
echo "Running demo..."
./bin/sudoku_demo
echo ""
if [ -f "./bin/sudoku_tests" ]; then
echo "Running tests..."
./bin/sudoku_tests
echo ""
else
echo "Tests not available (Google Test not found)"
echo ""
fi
if [ -f "./bin/sudoku_benchmarks" ]; then
echo "Running benchmarks..."
./bin/sudoku_benchmarks
else
echo "Benchmarks not available (Google Benchmark not found)"
fi
;;
*)
if [ -n "$1" ]; then
echo "Unknown option: $1"
echo "Usage: $0 [run|test|benchmark|all]"
fi
;;
esac

View File

@@ -64,7 +64,7 @@ int main()
Sudoku sudokuWorld = Sudoku{ "6......3.......7....7463....7.8...2.4...9...1.9...7.8....9851....6.......1......9" };
bool success = SudokuSolverCallback::Run(sudokuWorld, true);
bool success = WFC::Run<SudokuSolverCallback>(sudokuWorld, true);
bool solved = sudokuWorld.isSolved();

View File

@@ -33,7 +33,7 @@ protected:
// Helper function to solve a puzzle using WFC
void solvePuzzle(Sudoku& sudoku) {
SudokuSolver::Run(sudoku, true);
WFC::Run<SudokuSolver>(sudoku, true);
}
};
@@ -286,7 +286,7 @@ void testPuzzleSolving(const std::string& difficulty, const std::string& filenam
Sudoku& sudoku = puzzles[i];
EXPECT_TRUE(sudoku.isValid()) << difficulty << " puzzle " << i << " is not valid";
SudokuSolver::Run(sudoku);
WFC::Run<SudokuSolver>(sudoku);
EXPECT_TRUE(sudoku.isSolved()) << difficulty << " puzzle " << i << " was not solved. Puzzle string: " << sudoku.toString();

View File

@@ -26,6 +26,9 @@
namespace WFC {
struct EmptyInitialState {};
template<typename T>
concept WorldType = requires(T world, typename T::ValueType value) {
{ world.size() } -> std::integral;
@@ -44,265 +47,297 @@ concept HasConstexprSize = requires {
{ []() constexpr -> std::size_t { return WorldT{}.size(); }() };
};
template<typename WorldT, typename VarT,
typename VariableIDMapT = VariableIDMap<VarT>,
typename ConstrainerFunctionMapT = ConstrainerFunctionMap<void*>,
typename CallbacksT = Callbacks<WorldT>,
typename RandomSelectorT = DefaultRandomSelector<VarT>
>
class WFC {
public:
// Standalone SolverState struct
template <typename WorldT, typename RandomSelectorT = DefaultRandomSelector<typename WorldT::ValueType>>
struct SolverState {
using WorldType = WorldT;
using WorldSizeT = decltype(WorldT{}.size());
static constexpr WorldSizeT WorldSize = HasConstexprSize<WorldT> ? WorldT{}.size() : 0;
using PropagationQueueType = WFCQueue<WorldSize, WorldSizeT>;
WorldT& m_world;
PropagationQueueType m_propagationQueue{};
RandomSelectorT m_randomSelector{};
WFCStackAllocator m_allocator{};
size_t m_iterations{};
SolverState(WorldT& world, uint32_t seed)
: m_world(world)
, m_propagationQueue{ WorldSize ? WorldSize : static_cast<WorldSizeT>(world.size()) }
, m_randomSelector(seed)
{}
SolverState(const SolverState& other) = default;
};
// Types-only config struct produced by Builder
template <typename WorldT, typename VarT, typename VariableIDMapT,
typename ConstrainerFunctionMapT, typename CallbacksT, typename RandomSelectorT,
typename InitialStateFunctionT = EmptyInitialState>
struct WFCConfig {
static_assert(WorldType<WorldT>, "WorldT must satisfy World type requirements");
using WorldSizeT = decltype(WorldT{}.size());
// Try getting the world size, which is only available if the world type has a constexpr size() method
constexpr static WorldSizeT WorldSize = HasConstexprSize<WorldT> ? WorldT{}.size() : 0;
static constexpr WorldSizeT WorldSize = HasConstexprSize<WorldT> ? WorldT{}.size() : 0;
using SolverStateType = SolverState<WorldT, RandomSelectorT>;
using WaveType = Wave<VariableIDMapT, WorldSize>;
using PropagationQueueType = WFCQueue<WorldSize, WorldSizeT>;
using ConstrainerType = Constrainer<WaveType, PropagationQueueType>;
using MaskType = typename WaveType::ElementT;
using VariableIDT = typename WaveType::VariableIDT;
public:
struct SolverState
{
WorldT& m_world;
PropagationQueueType m_propagationQueue{};
RandomSelectorT m_randomSelector{};
WFCStackAllocator m_allocator{};
size_t m_iterations{};
SolverState(WorldT& world, uint32_t seed)
: m_world(world)
, m_propagationQueue{ WorldSize ? WorldSize : static_cast<WorldSizeT>(world.size()) }
, m_randomSelector(seed)
{}
SolverState(const SolverState& other) = default;
};
public:
WFC() = delete; // dont make an instance of this class, only use the static methods.
public:
static bool Run(WorldT& world, uint32_t seed = std::random_device{}())
{
SolverState state{ world, seed };
bool result = Run(state);
return result;
}
/**
* @brief Run the WFC algorithm to generate a solution
* @return true if a solution was found, false if contradiction occurred
*/
static bool Run(SolverState& state)
{
WaveType wave{ WorldSize, VariableIDMapT::size(), state.m_allocator };
PropogateInitialValues(state, wave);
if (RunLoop(state, wave)) {
PopulateWorld(state, wave);
return true;
}
return false;
}
static bool RunLoop(SolverState& state, WaveType& wave)
{
static constexpr size_t MaxIterations = 1024 * 8;
for (; state.m_iterations < MaxIterations; ++state.m_iterations)
{
if (!Propagate(state, wave))
return false;
if (wave.HasContradiction())
{
if constexpr (CallbacksT::HasContradictionCallback())
{
PopulateWorld(state, wave);
typename CallbacksT::ContradictionCallback{}(state.m_world);
}
return false;
}
if (wave.IsFullyCollapsed())
return true;
if constexpr (CallbacksT::HasBranchCallback())
{
PopulateWorld(state, wave);
typename CallbacksT::BranchCallback{}(state.m_world);
}
if (Branch(state, wave))
return true;
}
return false;
}
/**
* @brief Get the value at a specific cell
* @param cellId The cell ID
* @return The value if collapsed, std::nullopt otherwise
*/
static std::optional<VarT> GetValue(WaveType& wave, int cellId) {
if (wave.IsCollapsed(cellId)) {
auto variableId = 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
*/
static const std::vector<VarT> GetPossibleValues(WaveType& wave, int cellId)
{
std::vector<VarT> possibleValues;
MaskType mask = wave.GetMask(cellId);
for (size_t i = 0; i < ConstrainerFunctionMapT::size(); ++i) {
if (mask & (1 << i)) possibleValues.push_back(VariableIDMapT::GetValue(i));
}
return possibleValues;
}
private:
static void CollapseCell(SolverState& state, WaveType& wave, WorldSizeT cellId, VariableIDT value)
{
constexpr_assert(!wave.IsCollapsed(cellId) || wave.GetMask(cellId) == (MaskType(1) << value));
wave.Collapse(cellId, 1 << value);
constexpr_assert(wave.IsCollapsed(cellId));
if constexpr (CallbacksT::HasCellCollapsedCallback())
{
PopulateWorld(state, wave);
typename CallbacksT::CellCollapsedCallback{}(state.m_world);
}
}
static bool Branch(SolverState& state, WaveType& wave)
{
constexpr_assert(state.m_propagationQueue.empty());
// Find cell with minimum entropy > 1
WorldSizeT minEntropyCell = static_cast<WorldSizeT>(-1);
size_t minEntropy = static_cast<size_t>(-1);
for (WorldSizeT i = 0; i < wave.size(); ++i) {
size_t entropy = wave.Entropy(i);
if (entropy > 1 && entropy < minEntropy) {
minEntropy = entropy;
minEntropyCell = i;
}
}
if (minEntropyCell == static_cast<WorldSizeT>(-1)) return false;
constexpr_assert(!wave.IsCollapsed(minEntropyCell));
// create a list of possible values
VariableIDT availableValues = static_cast<VariableIDT>(wave.Entropy(minEntropyCell));
std::array<VariableIDT, VariableIDMapT::size()> possibleValues{};
MaskType mask = wave.GetMask(minEntropyCell);
for (size_t i = 0; i < availableValues; ++i)
{
VariableIDT index = static_cast<VariableIDT>(std::countr_zero(mask)); // get the index of the lowest set bit
constexpr_assert(index < VariableIDMapT::size(), "Possible value went outside bounds");
possibleValues[i] = index;
constexpr_assert(((mask & (MaskType(1) << index)) != 0), "Possible value was not set");
mask = mask & (mask - 1); // turn off lowest set bit
}
// randomly select a value from possible values
while (availableValues)
{
size_t randomIndex = state.m_randomSelector.rng(availableValues);
VariableIDT selectedValue = possibleValues[randomIndex];
{
// copy the state and branch out
auto stackFrame = state.m_allocator.createFrame();
auto queueFrame = state.m_propagationQueue.createBranchPoint();
auto newWave = wave;
CollapseCell(state, newWave, minEntropyCell, selectedValue);
state.m_propagationQueue.push(minEntropyCell);
if (RunLoop(state, newWave))
{
// move the solution to the original state
wave = newWave;
return true;
}
}
// remove the failure state from the wave
constexpr_assert((wave.GetMask(minEntropyCell) & (MaskType(1) << selectedValue)) != 0, "Possible value was not set");
wave.Collapse(minEntropyCell, ~(1 << selectedValue));
constexpr_assert((wave.GetMask(minEntropyCell) & (MaskType(1) << selectedValue)) == 0, "Wave was not collapsed correctly");
// swap replacement value with the last value
std::swap(possibleValues[randomIndex], possibleValues[--availableValues]);
}
return false;
}
static bool Propagate(SolverState& state, WaveType& wave)
{
while (!state.m_propagationQueue.empty())
{
WorldSizeT cellId = state.m_propagationQueue.pop();
if (wave.IsContradicted(cellId)) return false;
constexpr_assert(wave.IsCollapsed(cellId), "Cell was not collapsed");
VariableIDT variableID = wave.GetVariableID(cellId);
ConstrainerType constrainer(wave, state.m_propagationQueue);
using ConstrainerFunctionPtrT = void(*)(WorldT&, size_t, WorldValue<VarT>, ConstrainerType&);
ConstrainerFunctionMapT::template GetFunction<ConstrainerFunctionPtrT>(variableID)(state.m_world, cellId, WorldValue<VarT>{VariableIDMapT::GetValue(variableID), variableID}, constrainer);
}
return true;
}
static void PopulateWorld(SolverState& state, WaveType& wave)
{
for (size_t i = 0; i < wave.size(); ++i)
{
if (wave.IsCollapsed(i))
state.m_world.setValue(i, VariableIDMapT::GetValue(wave.GetVariableID(i)));
}
}
static void PropogateInitialValues(SolverState& state, WaveType& wave)
{
for (size_t i = 0; i < wave.size(); ++i)
{
for (size_t j = 0; j < VariableIDMapT::size(); ++j)
{
if (state.m_world.getValue(i) == VariableIDMapT::GetValue(j))
{
CollapseCell(state, wave, static_cast<WorldSizeT>(i), static_cast<VariableIDT>(j));
state.m_propagationQueue.push(i);
break;
}
}
}
}
using CallbacksType = CallbacksT;
using ConstrainerFunctionMapType = ConstrainerFunctionMapT;
using InitialStateFunctionType = InitialStateFunctionT;
static consteval bool HasInitialState() { return !std::is_same_v<InitialStateFunctionT, EmptyInitialState>; }
};
// Forward declarations for mutually recursive functions
template <typename CallbacksT, typename ConstrainerFunctionMapT, typename StateT, typename WaveT>
bool RunLoop(StateT& state, WaveT& wave);
template <typename CallbacksT, typename ConstrainerFunctionMapT, typename StateT, typename WaveT>
bool Branch(StateT& state, WaveT& wave);
namespace detail {
template <typename StateT, typename WaveT>
void PopulateWorld(StateT& state, WaveT& wave)
{
using VariableIDMapT = typename WaveT::IDMapT;
for (size_t i = 0; i < wave.size(); ++i)
{
if (wave.IsCollapsed(i))
state.m_world.setValue(i, VariableIDMapT::GetValue(wave.GetVariableID(i)));
}
}
template <typename CallbacksT, typename StateT, typename WaveT>
void CollapseCell(StateT& state, WaveT& wave, typename StateT::WorldSizeT cellId, typename WaveT::VariableIDT value)
{
using MaskType = typename WaveT::ElementT;
constexpr_assert(!wave.IsCollapsed(cellId) || wave.GetMask(cellId) == (MaskType(1) << value));
wave.Collapse(cellId, 1 << value);
constexpr_assert(wave.IsCollapsed(cellId));
if constexpr (CallbacksT::HasCellCollapsedCallback())
{
PopulateWorld(state, wave);
typename CallbacksT::CellCollapsedCallback{}(state.m_world);
}
}
template <typename CallbacksT, typename StateT, typename WaveT>
void PropogateInitialValues(StateT& state, WaveT& wave)
{
using VariableIDMapT = typename WaveT::IDMapT;
using WorldSizeT = typename StateT::WorldSizeT;
using VariableIDT = typename WaveT::VariableIDT;
for (size_t i = 0; i < wave.size(); ++i)
{
for (size_t j = 0; j < VariableIDMapT::size(); ++j)
{
if (state.m_world.getValue(i) == VariableIDMapT::GetValue(j))
{
CollapseCell<CallbacksT>(state, wave, static_cast<WorldSizeT>(i), static_cast<VariableIDT>(j));
state.m_propagationQueue.push(i);
break;
}
}
}
}
template <typename ConstrainerFunctionMapT, typename StateT, typename WaveT>
bool Propagate(StateT& state, WaveT& wave)
{
using VariableIDMapT = typename WaveT::IDMapT;
using VarT = typename VariableIDMapT::Type;
using WorldSizeT = typename StateT::WorldSizeT;
using VariableIDT = typename WaveT::VariableIDT;
using PropagationQueueType = typename StateT::PropagationQueueType;
using ConstrainerType = Constrainer<WaveT, PropagationQueueType>;
while (!state.m_propagationQueue.empty())
{
WorldSizeT cellId = state.m_propagationQueue.pop();
if (wave.IsContradicted(cellId)) return false;
constexpr_assert(wave.IsCollapsed(cellId), "Cell was not collapsed");
VariableIDT variableID = wave.GetVariableID(cellId);
ConstrainerType constrainer(wave, state.m_propagationQueue);
using WorldT = typename StateT::WorldType;
using ConstrainerFunctionPtrT = void(*)(WorldT&, size_t, WorldValue<VarT>, ConstrainerType&);
ConstrainerFunctionMapT::template GetFunction<ConstrainerFunctionPtrT>(variableID)(state.m_world, cellId, WorldValue<VarT>{VariableIDMapT::GetValue(variableID), variableID}, constrainer);
}
return true;
}
} // namespace detail
template <typename CallbacksT, typename ConstrainerFunctionMapT, typename StateT, typename WaveT>
bool Branch(StateT& state, WaveT& wave)
{
using VariableIDMapT = typename WaveT::IDMapT;
using MaskType = typename WaveT::ElementT;
using WorldSizeT = typename StateT::WorldSizeT;
using VariableIDT = typename WaveT::VariableIDT;
constexpr_assert(state.m_propagationQueue.empty());
// Find cell with minimum entropy > 1
WorldSizeT minEntropyCell = static_cast<WorldSizeT>(-1);
size_t minEntropy = static_cast<size_t>(-1);
for (WorldSizeT i = 0; i < wave.size(); ++i) {
size_t entropy = wave.Entropy(i);
if (entropy > 1 && entropy < minEntropy) {
minEntropy = entropy;
minEntropyCell = i;
}
}
if (minEntropyCell == static_cast<WorldSizeT>(-1)) return false;
constexpr_assert(!wave.IsCollapsed(minEntropyCell));
// create a list of possible values
VariableIDT availableValues = static_cast<VariableIDT>(wave.Entropy(minEntropyCell));
std::array<VariableIDT, VariableIDMapT::size()> possibleValues{};
MaskType mask = wave.GetMask(minEntropyCell);
for (size_t i = 0; i < availableValues; ++i)
{
VariableIDT index = static_cast<VariableIDT>(std::countr_zero(mask)); // get the index of the lowest set bit
constexpr_assert(index < VariableIDMapT::size(), "Possible value went outside bounds");
possibleValues[i] = index;
constexpr_assert(((mask & (MaskType(1) << index)) != 0), "Possible value was not set");
mask = mask & (mask - 1); // turn off lowest set bit
}
// randomly select a value from possible values
while (availableValues)
{
size_t randomIndex = state.m_randomSelector.rng(availableValues);
VariableIDT selectedValue = possibleValues[randomIndex];
{
// copy the state and branch out
auto stackFrame = state.m_allocator.createFrame();
auto queueFrame = state.m_propagationQueue.createBranchPoint();
auto newWave = wave;
detail::CollapseCell<CallbacksT>(state, newWave, minEntropyCell, selectedValue);
state.m_propagationQueue.push(minEntropyCell);
if (RunLoop<CallbacksT, ConstrainerFunctionMapT>(state, newWave))
{
// move the solution to the original state
wave = newWave;
return true;
}
}
// remove the failure state from the wave
constexpr_assert((wave.GetMask(minEntropyCell) & (MaskType(1) << selectedValue)) != 0, "Possible value was not set");
wave.Collapse(minEntropyCell, ~(1 << selectedValue));
constexpr_assert((wave.GetMask(minEntropyCell) & (MaskType(1) << selectedValue)) == 0, "Wave was not collapsed correctly");
// swap replacement value with the last value
std::swap(possibleValues[randomIndex], possibleValues[--availableValues]);
}
return false;
}
template <typename CallbacksT, typename ConstrainerFunctionMapT, typename StateT, typename WaveT>
bool RunLoop(StateT& state, WaveT& wave)
{
static constexpr size_t MaxIterations = 1024 * 8;
for (; state.m_iterations < MaxIterations; ++state.m_iterations)
{
if (!detail::Propagate<ConstrainerFunctionMapT>(state, wave))
return false;
if (wave.HasContradiction())
{
if constexpr (CallbacksT::HasContradictionCallback())
{
detail::PopulateWorld(state, wave);
typename CallbacksT::ContradictionCallback{}(state.m_world);
}
return false;
}
if (wave.IsFullyCollapsed())
return true;
if constexpr (CallbacksT::HasBranchCallback())
{
detail::PopulateWorld(state, wave);
typename CallbacksT::BranchCallback{}(state.m_world);
}
if (Branch<CallbacksT, ConstrainerFunctionMapT>(state, wave))
return true;
}
return false;
}
template <typename ConfigT>
bool Run(typename ConfigT::SolverStateType& state)
{
using CallbacksT = typename ConfigT::CallbacksType;
using ConstrainerFunctionMapT = typename ConfigT::ConstrainerFunctionMapType;
using WaveType = typename ConfigT::WaveType;
using VariableIDMapT = typename WaveType::IDMapT;
WaveType wave{ ConfigT::WorldSize, VariableIDMapT::size(), state.m_allocator };
detail::PropogateInitialValues<CallbacksT>(state, wave);
if constexpr (ConfigT::HasInitialState())
{
using ConstrainerType = Constrainer<WaveType, typename ConfigT::SolverStateType::PropagationQueueType>;
ConstrainerType constrainer(wave, state.m_propagationQueue);
typename ConfigT::InitialStateFunctionType{}(state.m_world, constrainer, state.m_randomSelector);
}
if (RunLoop<CallbacksT, ConstrainerFunctionMapT>(state, wave)) {
detail::PopulateWorld(state, wave);
return true;
}
return false;
}
template <typename ConfigT, typename WorldT>
bool Run(WorldT& world, uint32_t seed = std::random_device{}())
{
typename ConfigT::SolverStateType state{ world, seed };
return Run<ConfigT>(state);
}
template <typename WaveT>
std::optional<typename WaveT::IDMapT::Type> GetValue(WaveT& wave, int cellId) {
using VariableIDMapT = typename WaveT::IDMapT;
if (wave.IsCollapsed(cellId)) {
auto variableId = wave.GetVariableID(cellId);
return VariableIDMapT::GetValue(variableId);
}
return std::nullopt;
}
template <typename ConstrainerFunctionMapT, typename WaveT>
const std::vector<typename WaveT::IDMapT::Type> GetPossibleValues(WaveT& wave, int cellId)
{
using VariableIDMapT = typename WaveT::IDMapT;
using VarT = typename VariableIDMapT::Type;
using MaskType = typename WaveT::ElementT;
std::vector<VarT> possibleValues;
MaskType mask = wave.GetMask(cellId);
for (size_t i = 0; i < ConstrainerFunctionMapT::size(); ++i) {
if (mask & (1 << i)) possibleValues.push_back(VariableIDMapT::GetValue(i));
}
return possibleValues;
}
} // namespace WFC

View File

@@ -142,28 +142,31 @@ public:
public: // Sub byte
struct SubTypeAccess
{
constexpr SubTypeAccess(uint8_t& data, uint8_t subIndex) : Data{ data }, Shift{ StorageBits * subIndex } {};
constexpr SubTypeAccess(uint8_t& data, uint8_t subIndex) : Data{ data }, Shift{ static_cast<uint8_t>(StorageBits * subIndex) } {};
constexpr uint8_t GetValue() const { return ((Data >> Shift) & Mask); }
constexpr uint8_t SetValue(uint8_t val) { Clear(); return Data |= ((val & Mask) << Shift); }
constexpr void Clear() { Data &= ~Mask; }
constexpr uint8_t SetValue(uint8_t val) { Clear(); Data |= ((val & Mask) << Shift); return GetValue(); }
constexpr void Clear() { Data &= ~(static_cast<uint8_t>(Mask) << Shift); }
constexpr SubTypeAccess& operator=(uint8_t other) { return SetValue(other); }
constexpr SubTypeAccess& operator=(uint8_t other) { SetValue(other); return *this; }
constexpr operator uint8_t() const { return GetValue(); }
constexpr SubTypeAccess& operator&=(uint8_t other) { return SetValue(GetValue() & other); }
constexpr SubTypeAccess& operator|=(uint8_t other) { return SetValue(GetValue() | other); }
constexpr SubTypeAccess& operator^=(uint8_t other) { return SetValue(GetValue() ^ other); }
constexpr SubTypeAccess& operator<<=(uint8_t other) { return SetValue(GetValue() << other); }
constexpr SubTypeAccess& operator>>=(uint8_t other) { return SetValue(GetValue() >> other); }
constexpr SubTypeAccess& operator&=(uint8_t other) { SetValue(GetValue() & other); return *this; }
constexpr SubTypeAccess& operator|=(uint8_t other) { SetValue(GetValue() | other); return *this; }
constexpr SubTypeAccess& operator^=(uint8_t other) { SetValue(GetValue() ^ other); return *this; }
constexpr SubTypeAccess& operator<<=(uint8_t other) { SetValue(GetValue() << other); return *this; }
constexpr SubTypeAccess& operator>>=(uint8_t other) { SetValue(GetValue() >> other); return *this; }
uint8_t& Data;
uint8_t Shift;
};
constexpr const SubTypeAccess operator[](size_t index) const requires(IsSubByte) { return SubTypeAccess{data()[index / ElementsPerByte], index & ElementsPerByte }; }
constexpr SubTypeAccess operator[](size_t index) requires(IsSubByte) { return SubTypeAccess{data()[index / ElementsPerByte], index & ElementsPerByte }; }
constexpr StorageType operator[](size_t index) const requires(IsSubByte) {
uint8_t shift = static_cast<uint8_t>(StorageBits * (index % ElementsPerByte));
return (data()[index / ElementsPerByte] >> shift) & static_cast<StorageType>(Mask);
}
constexpr SubTypeAccess operator[](size_t index) requires(IsSubByte) { return SubTypeAccess{data()[index / ElementsPerByte], static_cast<uint8_t>(index % ElementsPerByte) }; }
public: // default
constexpr const StorageType& operator[](size_t index) const requires(!IsSubByte) { return data()[index]; }

View File

@@ -13,13 +13,14 @@ namespace WFC {
* @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>,
typename WorldT,
typename VarT = typename WorldT::ValueType,
typename VariableIDMapT = VariableIDMap<VarT>,
typename ConstrainerFunctionMapT = ConstrainerFunctionMap<void*>,
typename CallbacksT = Callbacks<WorldT>,
typename RandomSelectorT = DefaultRandomSelector<VarT>,
typename SelectedValueT = void>
typename SelectedValueT = void,
typename InitialStateFunctionT = EmptyInitialState>
class Builder {
public:
using WorldSizeT = decltype(WorldT{}.size());
@@ -31,22 +32,22 @@ public:
template <VarT ... Values>
using DefineIDs = Builder<WorldT, VarT, VariableIDMap<VarT, Values...>, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT, VariableIDMap<VarT, Values...>>;
using DefineIDs = Builder<WorldT, VarT, VariableIDMap<VarT, Values...>, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT, VariableIDMap<VarT, Values...>, InitialStateFunctionT>;
template <size_t RangeStart, size_t RangeEnd>
using DefineRange = Builder<WorldT, VarT, VariableIDRange<VarT, RangeStart, RangeEnd>, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT, VariableIDRange<VarT, RangeStart, RangeEnd>>;
using DefineRange = Builder<WorldT, VarT, VariableIDRange<VarT, RangeStart, RangeEnd>, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT, VariableIDRange<VarT, RangeStart, RangeEnd>, InitialStateFunctionT>;
template <size_t RangeEnd>
using DefineRange0 = Builder<WorldT, VarT, VariableIDRange<VarT, 0, RangeEnd>, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT, VariableIDRange<VarT, 0, RangeEnd>>;
using DefineRange0 = Builder<WorldT, VarT, VariableIDRange<VarT, 0, RangeEnd>, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT, VariableIDRange<VarT, 0, RangeEnd>, InitialStateFunctionT>;
template <VarT ... Values>
using Variable = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT, VariableIDMap<VarT, Values...>>;
using Variable = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT, VariableIDMap<VarT, Values...>, InitialStateFunctionT>;
template <size_t RangeStart, size_t RangeEnd>
using VariableRange = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT, VariableIDRange<VarT, RangeStart, RangeEnd>>;
using VariableRange = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT, VariableIDRange<VarT, RangeStart, RangeEnd>, InitialStateFunctionT>;
using EmptyConstrainerFunctionT = EmptyConstrainerFunction<WorldT, WorldSizeT, VarT, ConstrainerType>;
template <typename ConstrainerFunctionT>
@@ -58,7 +59,7 @@ public:
ConstrainerFunctionT,
SelectedValueT,
EmptyConstrainerFunctionT
>, CallbacksT, RandomSelectorT, SelectedValueT
>, CallbacksT, RandomSelectorT, SelectedValueT, InitialStateFunctionT
>;
template <typename ConstrainerFunctionT>
@@ -70,22 +71,25 @@ public:
ConstrainerFunctionT,
VariableIDMapT,
EmptyConstrainerFunctionT
>, CallbacksT, RandomSelectorT
>, CallbacksT, RandomSelectorT, SelectedValueT, InitialStateFunctionT
>;
template <typename NewCellCollapsedCallbackT>
using SetCellCollapsedCallback = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, typename CallbacksT::template SetCellCollapsedCallbackT<NewCellCollapsedCallbackT>, RandomSelectorT>;
using SetCellCollapsedCallback = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, typename CallbacksT::template SetCellCollapsedCallbackT<NewCellCollapsedCallbackT>, RandomSelectorT, SelectedValueT, InitialStateFunctionT>;
template <typename NewContradictionCallbackT>
using SetContradictionCallback = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, typename CallbacksT::template SetContradictionCallbackT<NewContradictionCallbackT>, RandomSelectorT>;
using SetContradictionCallback = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, typename CallbacksT::template SetContradictionCallbackT<NewContradictionCallbackT>, RandomSelectorT, SelectedValueT, InitialStateFunctionT>;
template <typename NewBranchCallbackT>
using SetBranchCallback = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, typename CallbacksT::template SetBranchCallbackT<NewBranchCallbackT>, RandomSelectorT>;
using SetBranchCallback = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, typename CallbacksT::template SetBranchCallbackT<NewBranchCallbackT>, RandomSelectorT, SelectedValueT, InitialStateFunctionT>;
template <typename NewRandomSelectorT>
using SetRandomSelector = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, CallbacksT, NewRandomSelectorT>;
using SetRandomSelector = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, CallbacksT, NewRandomSelectorT, SelectedValueT, InitialStateFunctionT>;
using Build = WFC<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT>;
template <typename NewInitialStateFunctionT>
using SetInitialState = Builder<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT, SelectedValueT, NewInitialStateFunctionT>;
using Build = WFCConfig<WorldT, VarT, VariableIDMapT, ConstrainerFunctionMapT, CallbacksT, RandomSelectorT, InitialStateFunctionT>;
};
}
}

View File

@@ -8,7 +8,11 @@ namespace WFC {
template <typename WorldT, typename WorldSizeT, typename VarT, typename ConstainerType>
struct EmptyConstrainerFunction
{
static void invoke(WorldT&, WorldSizeT, WorldValue<VarT>, ConstainerType&) {}
void operator()(WorldT&, WorldSizeT, WorldValue<VarT>, ConstainerType&) const {}
using FuncPtrType = void(*)(WorldT&, WorldSizeT, WorldValue<VarT>, ConstainerType&);
operator FuncPtrType() const { return &invoke; }
};
template <typename ... ConstrainerFunctions>
@@ -113,6 +117,23 @@ public:
ApplyMask(cellId, 1 << value.InternalIndex);
}
/**
* @brief Re-enable specific values for a cell (OR bits back in)
* @param cellId The ID of the cell to modify
*/
template <typename IDMapT::Type ... IncludedValues>
void Include(size_t cellId) {
static_assert(sizeof...(IncludedValues) > 0, "At least one included value must be provided");
if (m_wave.IsCollapsed(cellId)) return; // don't un-collapse decided cells
auto indices = IDMapT::template ValuesToIndices<IncludedValues...>();
m_wave.Enable(cellId, BitContainerT::GetMask(indices));
}
void Include(WorldValue<typename IDMapT::Type> value, size_t cellId) {
if (m_wave.IsCollapsed(cellId)) return;
m_wave.Enable(cellId, 1 << value.InternalIndex);
}
private:
void ApplyMask(size_t cellId, MaskType mask) {
bool wasCollapsed = m_wave.IsCollapsed(cellId);

View File

@@ -1,5 +1,7 @@
#pragma once
#include <random>
namespace WFC {
/**
@@ -27,14 +29,14 @@ public:
template <typename VarT>
class AdvancedRandomSelector {
private:
std::mt19937& m_rng;
mutable std::mt19937 m_rng;
public:
explicit AdvancedRandomSelector(std::mt19937& rng) : m_rng(rng) {}
explicit AdvancedRandomSelector(uint32_t seed = 0x12345678) : m_rng(seed) {}
uint32_t rng(uint32_t max) const {
std::uniform_int_distribution<uint32_t> dist(0, max);
return dist(m_rng);
uint32_t rng(uint32_t max) const {
std::uniform_int_distribution<uint32_t> dist(0, max - 1);
return dist(m_rng);
}
};

View File

@@ -19,6 +19,7 @@ using VariableIDType = std::conditional_t<VariablesAmount <= std::numeric_limits
template <typename VarT, VarT ... Values>
class VariableIDMap {
public:
using Type = VarT;
template <VarT ... AdditionalValues>
using Merge = VariableIDMap<VarT, Values..., AdditionalValues...>;
@@ -60,7 +61,8 @@ public:
static constexpr VarT GetValue(size_t index) {
constexpr_assert(index < size());
return GetAllValues()[index];
constexpr VarT arr[] = {Values...};
return arr[index];
}
static consteval size_t size() { return sizeof...(Values); }

View File

@@ -18,20 +18,29 @@ public:
public:
Wave() = default;
Wave(size_t size, size_t variableAmount, WFCStackAllocator& allocator) : m_data(size, allocator)
Wave(size_t size, size_t variableAmount, WFCStackAllocator& allocator) : m_data(size, allocator)
{
for (auto& wave : m_data) wave = (1 << variableAmount) - 1;
for (size_t i = 0; i < m_data.size(); ++i) m_data[i] = (1 << variableAmount) - 1;
}
Wave(const Wave& other) = default;
public:
void Collapse(size_t index, ElementT mask) { m_data[index] &= mask; }
void Enable(size_t index, ElementT 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(), [](ElementT value) { return std::popcount(value) == 1; }); }
bool HasContradiction() const { return std::any_of(m_data.begin(), m_data.end(), [](ElementT value) { return value == 0; }); }
bool IsFullyCollapsed() const {
for (size_t i = 0; i < m_data.size(); ++i)
if (std::popcount(static_cast<ElementT>(m_data[i])) != 1) return false;
return true;
}
bool HasContradiction() const {
for (size_t i = 0; i < m_data.size(); ++i)
if (static_cast<ElementT>(m_data[i]) == 0) return true;
return false;
}
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])); }
ElementT GetMask(size_t index) const { return m_data[index]; }

View File

@@ -9,25 +9,25 @@ namespace WFC {
/**
* @brief 2D Array World implementation
*/
template<typename T, size_t Width, size_t Height>
template<typename T, size_t Width, size_t Height, bool Looping = false>
class Array2D {
public:
using ValueType = T;
using CoordType = std::tuple<int, int>;
Array2D() = default;
constexpr Array2D() = default;
/**
* @brief Get the total size of the world
*/
size_t size() const {
constexpr size_t size() const {
return Width * Height;
}
/**
* @brief Convert coordinates to cell ID
*/
int getId(CoordType coord) const {
constexpr int getId(CoordType coord) const {
auto [x, y] = coord;
return y * Width + x;
}
@@ -35,66 +35,86 @@ public:
/**
* @brief Convert cell ID to coordinates
*/
CoordType getCoord(int id) const {
constexpr CoordType getCoord(int id) const {
int x = id % Width;
int y = id / Width;
return {x, y};
}
constexpr size_t GetCoordOffsetX(int x, int dx) const {
if constexpr (Looping) {
return (x + dx + Width) % Width;
} else {
return static_cast<size_t>(std::clamp(x + dx, 0, static_cast<int>(Width) - 1));
}
}
constexpr size_t GetCoordOffsetY(int y, int dy) const {
if constexpr (Looping) {
return (y + dy + Width) % Width;
} else {
return static_cast<size_t>(std::clamp(y + dy, 0, static_cast<int>(Height) - 1));
}
}
constexpr size_t getCoordOffset(int x, int y, int dx, int dy) const {
return getId({GetCoordOffsetX(x, dx), GetCoordOffsetY(y, dy)});
}
/**
* @brief Get width of the array
*/
size_t width() const { return Width; }
constexpr size_t width() const { return Width; }
/**
* @brief Get height of the array
*/
size_t height() const { return Height; }
constexpr size_t height() const { return Height; }
/**
* @brief Access element at coordinates
*/
T& at(int x, int y) {
constexpr 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 {
constexpr const T& at(int x, int y) const {
return data_[y * Width + x];
}
/**
* @brief Access element by ID
*/
T& operator[](int id) {
constexpr T& operator[](int id) {
return data_[id];
}
/**
* @brief Access element by ID (const)
*/
const T& operator[](int id) const {
constexpr 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) {
constexpr void setValue(size_t index, T value) {
data_[index] = value;
}
/**
* @brief Get value at specific index
*/
T getValue(size_t index) const {
constexpr T getValue(size_t index) const {
return data_[index];
}
private:
std::array<T, Width * Height> data_;
std::array<T, Width * Height> data_{};
};
/**
@@ -106,19 +126,19 @@ public:
using ValueType = T;
using CoordType = std::tuple<int, int, int>;
Array3D() = default;
constexpr Array3D() = default;
/**
* @brief Get the total size of the world
*/
size_t size() const {
constexpr size_t size() const {
return Width * Height * Depth;
}
/**
* @brief Convert coordinates to cell ID
*/
int getId(CoordType coord) const {
constexpr int getId(CoordType coord) const {
auto [x, y, z] = coord;
return z * (Width * Height) + y * Width + x;
}
@@ -126,7 +146,7 @@ public:
/**
* @brief Convert cell ID to coordinates
*/
CoordType getCoord(int id) const {
constexpr CoordType getCoord(int id) const {
int x = id % Width;
int y = (id / Width) % Height;
int z = id / (Width * Height);
@@ -136,48 +156,47 @@ public:
/**
* @brief Access element at coordinates
*/
T& at(int x, int y, int z) {
constexpr 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 {
constexpr 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) {
constexpr T& operator[](int id) {
return data_[id];
}
/**
* @brief Access element by ID (const)
*/
const T& operator[](int id) const {
constexpr 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) {
constexpr void setValue(size_t index, T value) {
data_[index] = value;
}
/**
* @brief Get value at specific index
*/
T getValue(size_t index) const {
constexpr T getValue(size_t index) const {
return data_[index];
}
private:
std::array<T, Width * Height * Depth> data_;
std::array<T, Width * Height * Depth> data_{};
};
} // namespace WFC