Merge commit '2616eaf57a1c3facda4e7b11db092e03b0b652ac'

This commit is contained in:
cdemeyer-teachx
2025-08-22 11:18:36 +09:00
25 changed files with 1249654 additions and 59 deletions

View File

@@ -10,8 +10,8 @@ set(CMAKE_CXX_EXTENSIONS OFF)
enable_testing()
# Options
option(ND_WFC_BUILD_TESTS "Build tests" OFF) # Temporarily disabled due to hash issue
option(ND_WFC_BUILD_EXAMPLES "Build examples" OFF) # Temporarily disabled
option(ND_WFC_BUILD_TESTS "Build tests" OFF) # Temporarily disabled due to gtest hash issue
option(ND_WFC_BUILD_EXAMPLES "Build examples" ON)
option(ND_WFC_USE_SYSTEM_LIBS "Use system libraries instead of bundled" OFF)
# Add subdirectories

105
demos/sudoku/CMakeLists.txt Normal file
View File

@@ -0,0 +1,105 @@
cmake_minimum_required(VERSION 3.16)
project(sudoku_demo CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Enable testing
enable_testing()
# Add compiler warnings
if(MSVC)
add_compile_options(/W4)
else()
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# Find Google Test (optional)
find_package(GTest)
if(GTest_FOUND)
include_directories(${GTEST_INCLUDE_DIRS})
set(HAS_GTEST TRUE)
else()
set(HAS_GTEST FALSE)
message(WARNING "Google Test not found. Tests will not be built.")
endif()
# Find Google Benchmark (optional)
find_package(benchmark)
if(benchmark_FOUND)
set(HAS_BENCHMARK TRUE)
else()
set(HAS_BENCHMARK FALSE)
message(WARNING "Google Benchmark not found. Benchmarks will not be built.")
endif()
# Create the main executable
add_executable(sudoku_demo
main.cpp
sudoku.cpp
)
# Set output directory
set_target_properties(sudoku_demo PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
# Include directories
target_include_directories(sudoku_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# Optional: Enable optimizations for release builds
if(CMAKE_BUILD_TYPE STREQUAL "Release")
if(MSVC)
target_compile_options(sudoku_demo PRIVATE /O2)
else()
target_compile_options(sudoku_demo PRIVATE -O3 -march=native)
endif()
endif()
# Create test executable (if GTest is available)
if(HAS_GTEST)
add_executable(sudoku_tests
sudoku.cpp
test_sudoku.cpp
)
target_link_libraries(sudoku_tests ${GTEST_LIBRARIES} pthread)
# Set test output directory
set_target_properties(sudoku_tests PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
# Include directories for tests
target_include_directories(sudoku_tests PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# Add test to CTest
add_test(NAME sudoku_tests COMMAND sudoku_tests)
endif()
# Create benchmark executable (if Google Benchmark is available)
if(HAS_BENCHMARK)
add_executable(sudoku_benchmarks
sudoku.cpp
benchmark_sudoku.cpp
)
target_link_libraries(sudoku_benchmarks ${GTEST_LIBRARIES} benchmark::benchmark pthread)
# Set benchmark output directory
set_target_properties(sudoku_benchmarks PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
# Include directories for benchmarks
target_include_directories(sudoku_benchmarks PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
endif()
# Installation (optional)
if(HAS_GTEST AND HAS_BENCHMARK)
install(TARGETS sudoku_demo sudoku_tests sudoku_benchmarks DESTINATION bin)
elseif(HAS_GTEST)
install(TARGETS sudoku_demo sudoku_tests DESTINATION bin)
else()
install(TARGETS sudoku_demo DESTINATION bin)
endif()

316
demos/sudoku/README.md Normal file
View File

@@ -0,0 +1,316 @@
# Fast Sudoku Loader and Solution Validator
A memory-efficient and fast Sudoku implementation with puzzle loading and solution validation capabilities.
## Features
- **Ultra-Memory Efficient**: Uses exactly 41 bytes per Sudoku instance
- 4-bit packed storage (81 cells × 4 bits = 324 bits = 40.5 bytes → 41 bytes)
- Static assertion ensures exactly 41 bytes
- ~70% memory reduction compared to previous implementation
- **Ultra-Fast Operations**: Bitwise-only operations with strategic inlining and hot path optimizations
- **Multiple Input Formats**: Load from strings, files, or directories
- **Comprehensive Validation**: Check validity, completeness, and find conflicts
- **Easy to Use**: Simple API with comprehensive examples
## Memory Usage
```
Sudoku Class: Exactly 41 bytes total
└── 4-bit packed board: 41 bytes (81 cells × 4 bits = 324 bits)
Memory savings: 94 bytes per instance (69.6% reduction)
```
## Quick Start
### Building
```bash
cd /home/connor/repos/nd-wfc/demos/sudoku
mkdir build && cd build
cmake ..
make
./bin/sudoku_demo
```
### Basic Usage
```cpp
#include "sudoku.h"
// Create and load a puzzle from string
Sudoku sudoku("530070000600195000098000060800060003400803001700020006060000280000419005000080079");
// Check if it's valid
if (sudoku.isValid()) {
std::cout << "Valid puzzle!" << std::endl;
}
// Make a move
if (sudoku.set(0, 1, 2)) { // Set row 0, col 1 to 2
std::cout << "Move successful!" << std::endl;
}
// Check if solved
if (sudoku.isSolved()) {
std::cout << "Puzzle solved!" << std::endl;
}
```
### Loading Puzzles
```cpp
// From string
auto sudoku1 = SudokuLoader::fromString("530070000600195000098000060800060003400803001700020006060000280000419005000080079");
// From file
auto sudoku2 = SudokuLoader::fromFile("puzzle.txt");
// From directory (all .txt files)
auto puzzles = SudokuLoader::fromDirectory("./puzzles/", ".txt");
```
### Validating Solutions
```cpp
// Stateless validation using raw board data
Sudoku::Board board = sudoku.getBoard();
if (SudokuValidator::isValidSolution(board)) {
std::cout << "Valid complete solution!" << std::endl;
}
if (SudokuValidator::hasConflicts(board)) {
auto conflicts = SudokuValidator::findConflicts(board);
std::cout << "Found " << conflicts.size() << " conflicts" << std::endl;
}
```
## Input Formats
### String Format
81 characters, using:
- `0` or `.` for empty cells
- `1-9` for filled cells
Example:
```
530070000600195000098000060800060003400803001700020006060000280000419005000080079
```
### File Format
Supports comments (#) and whitespace. Can be split across multiple lines.
Example:
```
# Easy Sudoku Puzzle
530070000
600195000
098000060
800060003
400803001
700020006
060000280
000419005
000080079
```
## API Reference
### Sudoku Class
#### Constructors
- `Sudoku()` - Create empty puzzle
- `Sudoku(const std::string& puzzle_str)` - Create from string
#### Loading
- `bool loadFromString(const std::string& puzzle_str)`
- `bool loadFromFile(const std::string& filename)`
#### Board Operations (Inlined for Performance)
- `inline uint8_t get(int row, int col)` - Get cell value with bounds assertion (ultra-fast)
- `inline bool set(int row, int col, uint8_t value)` - Set cell value with bounds assertion (ultra-fast)
- `void clear()` - Clear the board
#### Validation
- `bool isValid()` - Check if current board is valid
- `bool isSolved()` - Check if puzzle is complete and valid
- `inline bool isValidMove(int row, int col, uint8_t value)` - Check if move is valid (inlined for performance)
#### Utilities
- `void print()` - Print board to console
- `std::string toString()` - Convert to string
- `std::array<uint8_t, 81> getBoard()` - Convert to standard board format
### SudokuValidator Class (Static)
#### Validation Functions
- `bool isValidSolution(const std::array<uint8_t, 81>& board)` - Check complete solution
- `bool isValidPartial(const std::array<uint8_t, 81>& board)` - Check partial solution
- `bool hasConflicts(const std::array<uint8_t, 81>& board)` - Check for any conflicts
- `std::vector<std::pair<int, int>> findConflicts(const std::array<uint8_t, 81>& board)` - Find conflicting positions
### SudokuLoader Class (Static)
#### Loading Functions
- `std::optional<Sudoku> fromString(const std::string& puzzle_str)`
- `std::optional<Sudoku> fromFile(const std::string& filename)`
- `std::vector<Sudoku> fromDirectory(const std::string& dirname, const std::string& extension = ".txt")`
#### Storage Implementation
- `uint8_t get(int pos)` - Get value with range assertion (0-9)
- `void set(int pos, uint8_t value)` - Set value with range assertion (0-9)
#### Private Helper Functions
- `bool parseLine(const std::string& line, std::array<uint8_t, 81>& board)`
## Sample Puzzles
### Easy Puzzle
```
530070000600195000098000060800060003400803001700020006060000280000419005000080079
```
### Medium Puzzle
```
003020600900305001001806400008102900700000008006708200002609500800203009005010300
```
### Hard Puzzle
```
400000805030000000000700000020000060000080400000010000000603070500200000104000000
```
### Solved Puzzle
```
534678912672195348198342567859761423426853791713924856961537284287419635345286179
```
## Performance
The implementation is optimized for speed and memory usage:
- **Validation**: ~0.11 microseconds per check
- **Memory**: Exactly 41 bytes per instance
- **Move validation**: Ultra-fast inlined bitwise operations only
- **Conflict detection**: Inlined direct board scanning with `std::bitset<10>` optimization
- **Position bounds**: Assert-based checking (zero overhead in release)
Run the demo to see performance benchmarks:
```bash
./bin/sudoku_demo
```
## Testing and Benchmarking
The project includes comprehensive Google Test and Google Benchmark integration:
**Note**: Google Test and Google Benchmark are optional dependencies. If not found on your system, the project will still build and run the main demo, but tests and benchmarks will not be available.
### Installing Dependencies (Optional)
#### Ubuntu/Debian:
```bash
# Install Google Test
sudo apt-get install libgtest-dev
# Install Google Benchmark
sudo apt-get install libbenchmark-dev
```
#### macOS (with Homebrew):
```bash
# Install Google Test
brew install googletest
# Install Google Benchmark
brew install google-benchmark
```
#### Windows (with vcpkg):
```bash
# Install Google Test
vcpkg install gtest
# Install Google Benchmark
vcpkg install benchmark
```
### Running Tests
```bash
# Build and run all tests
./build.sh test
# Or run tests directly
./bin/sudoku_tests
```
### Running Benchmarks
```bash
# Build and run all benchmarks
./build.sh benchmark
# Or run benchmarks directly
./bin/sudoku_benchmarks
```
### Available Tests
- **Basic functionality**: Empty sudoku, set/get operations, loading
- **Validation**: Move validation, conflict detection, solved puzzles
- **Edge cases**: Invalid inputs, boundary conditions
- **Performance**: Timing tests for operations
- **Memory**: Size validation (41 bytes)
### Available Benchmarks
- **Core operations**: get/set performance
- **Validation**: isValid/isValidMove timing
- **Conversion**: toString/getBoard performance
- **Puzzle complexity**: Easy/medium/hard/solved puzzle benchmarks
- **Memory operations**: Allocation/deallocation timing
- **Validator functions**: Static validation performance
### Test Output Example
```
[==========] Running 18 tests from 1 test case.
[----------] Global test environment set-up.
[----------] 18 tests from SudokuTest (X ms total)
[ RUN ] SudokuTest.MemorySize
[ OK ] SudokuTest.MemorySize (0 ms)
[ RUN ] SudokuTest.SetAndGet
[ OK ] SudokuTest.SetAndGet (0 ms)
...
[==========] 18 tests from 1 test case ran. (X ms total)
[ PASSED ] 18 tests.
```
## Building and Running
```bash
# Navigate to the sudoku demo directory
cd /home/connor/repos/nd-wfc/demos/sudoku
# Create build directory
mkdir build && cd build
# Configure with CMake
cmake ..
# Build
make
# Run demo
./bin/sudoku_demo
```
## Testing
The demo program includes comprehensive tests for:
- Basic functionality
- Memory efficiency
- Performance benchmarks
- Loading from various formats
- Validation accuracy
Run the demo to see all tests in action.

View File

@@ -0,0 +1,225 @@
#include <benchmark/benchmark.h>
#include "sudoku.h"
// Benchmark fixture for Sudoku benchmarks
class SudokuBenchmark : public benchmark::Fixture {
public:
void SetUp(const ::benchmark::State& state) override {
// Create test puzzle
testPuzzle = "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
sudoku.loadFromString(testPuzzle);
}
void TearDown(const ::benchmark::State& state) override {
// Cleanup if needed
}
Sudoku sudoku;
std::string testPuzzle;
};
// Benchmark get operations
BENCHMARK_F(SudokuBenchmark, GetOperations)(benchmark::State& state) {
for (auto _ : state) {
// Perform get operations on all cells
for (int row = 0; row < 9; ++row) {
for (int col = 0; col < 9; ++col) {
benchmark::DoNotOptimize(sudoku.get(row, col));
}
}
}
}
// Benchmark set operations
BENCHMARK_F(SudokuBenchmark, SetOperations)(benchmark::State& state) {
for (auto _ : state) {
// Clear and set all cells
sudoku.clear();
for (int row = 0; row < 9; ++row) {
for (int col = 0; col < 9; ++col) {
sudoku.set(row, col, (row + col) % 9 + 1);
}
}
}
}
// Benchmark validation operations
BENCHMARK_F(SudokuBenchmark, IsValidOperation)(benchmark::State& state) {
for (auto _ : state) {
benchmark::DoNotOptimize(sudoku.isValid());
}
}
// Benchmark isValidMove operations (includes conflict checking internally)
BENCHMARK_F(SudokuBenchmark, IsValidMoveOperation)(benchmark::State& state) {
for (auto _ : state) {
// Test various move validations
for (int row = 0; row < 9; ++row) {
for (int col = 0; col < 9; ++col) {
benchmark::DoNotOptimize(sudoku.isValidMove(row, col, (row + col) % 9 + 1));
}
}
}
}
// Benchmark loading from string
BENCHMARK_F(SudokuBenchmark, LoadFromString)(benchmark::State& state) {
for (auto _ : state) {
Sudoku tempSudoku;
tempSudoku.loadFromString(testPuzzle);
benchmark::DoNotOptimize(tempSudoku);
}
}
// Benchmark toString conversion
BENCHMARK_F(SudokuBenchmark, ToStringConversion)(benchmark::State& state) {
for (auto _ : state) {
std::string str = sudoku.toString();
benchmark::DoNotOptimize(str);
}
}
// Benchmark getBoard conversion
BENCHMARK_F(SudokuBenchmark, GetBoardConversion)(benchmark::State& state) {
for (auto _ : state) {
auto board = sudoku.getBoard();
benchmark::DoNotOptimize(board);
}
}
// Benchmark memory operations
BENCHMARK_F(SudokuBenchmark, MemoryOperations)(benchmark::State& state) {
for (auto _ : state) {
// Test memory allocation and deallocation
Sudoku* temp = new Sudoku();
temp->loadFromString(testPuzzle);
delete temp;
}
}
// Benchmark with different puzzle complexities
static void BM_EasyPuzzle(benchmark::State& state) {
Sudoku sudoku;
std::string easy = "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
sudoku.loadFromString(easy);
for (auto _ : state) {
for (int row = 0; row < 9; ++row) {
for (int col = 0; col < 9; ++col) {
benchmark::DoNotOptimize(sudoku.get(row, col));
}
}
}
}
static void BM_MediumPuzzle(benchmark::State& state) {
Sudoku sudoku;
std::string medium = "003020600900305001001806400008102900700000008006708200002609500800203009005010300";
sudoku.loadFromString(medium);
for (auto _ : state) {
for (int row = 0; row < 9; ++row) {
for (int col = 0; col < 9; ++col) {
benchmark::DoNotOptimize(sudoku.get(row, col));
}
}
}
}
static void BM_HardPuzzle(benchmark::State& state) {
Sudoku sudoku;
std::string hard = "400000805030000000000700000020000060000080400000010000000603070500200000104000000";
sudoku.loadFromString(hard);
for (auto _ : state) {
for (int row = 0; row < 9; ++row) {
for (int col = 0; col < 9; ++col) {
benchmark::DoNotOptimize(sudoku.get(row, col));
}
}
}
}
static void BM_SolvedPuzzle(benchmark::State& state) {
Sudoku sudoku;
std::string solved = "534678912672195348198342567859761423426853791713924856961537284287419635345286179";
sudoku.loadFromString(solved);
for (auto _ : state) {
for (int row = 0; row < 9; ++row) {
for (int col = 0; col < 9; ++col) {
benchmark::DoNotOptimize(sudoku.get(row, col));
}
}
}
}
// Register different puzzle complexity benchmarks
BENCHMARK(BM_EasyPuzzle)->Name("Easy_Puzzle_Get");
BENCHMARK(BM_MediumPuzzle)->Name("Medium_Puzzle_Get");
BENCHMARK(BM_HardPuzzle)->Name("Hard_Puzzle_Get");
BENCHMARK(BM_SolvedPuzzle)->Name("Solved_Puzzle_Get");
// Benchmark SudokuValidator functions
static void BM_ValidatorValidSolution(benchmark::State& state) {
std::array<uint8_t, 81> solved = {
5,3,4,6,7,8,9,1,2,
6,7,2,1,9,5,3,4,8,
1,9,8,3,4,2,5,6,7,
8,5,9,7,6,1,4,2,3,
4,2,6,8,5,3,7,9,1,
7,1,3,9,2,4,8,5,6,
9,6,1,5,3,7,2,8,4,
2,8,7,4,1,9,6,3,5,
3,4,5,2,8,6,1,7,9
};
for (auto _ : state) {
benchmark::DoNotOptimize(SudokuValidator::isValidSolution(solved));
}
}
static void BM_ValidatorHasConflicts(benchmark::State& state) {
std::array<uint8_t, 81> solved = {
5,3,4,6,7,8,9,1,2,
6,7,2,1,9,5,3,4,8,
1,9,8,3,4,2,5,6,7,
8,5,9,7,6,1,4,2,3,
4,2,6,8,5,3,7,9,1,
7,1,3,9,2,4,8,5,6,
9,6,1,5,3,7,2,8,4,
2,8,7,4,1,9,6,3,5,
3,4,5,2,8,6,1,7,9
};
for (auto _ : state) {
benchmark::DoNotOptimize(SudokuValidator::hasConflicts(solved));
}
}
BENCHMARK(BM_ValidatorValidSolution)->Name("Validator_ValidSolution");
BENCHMARK(BM_ValidatorHasConflicts)->Name("Validator_HasConflicts");
// Memory usage benchmark
static void BM_MemoryUsage(benchmark::State& state) {
for (auto _ : state) {
state.PauseTiming();
std::vector<Sudoku> sudokus;
state.ResumeTiming();
// Allocate many Sudoku instances to measure memory usage
for (int i = 0; i < state.range(0); ++i) {
sudokus.emplace_back();
}
state.PauseTiming();
sudokus.clear();
state.ResumeTiming();
}
}
BENCHMARK(BM_MemoryUsage)->Name("Memory_Usage")->Range(1000, 100000)->Unit(benchmark::kMicrosecond);
BENCHMARK_MAIN();

93
demos/sudoku/build.sh Executable file
View File

@@ -0,0 +1,93 @@
#!/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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

329
demos/sudoku/sudoku.cpp Normal file
View File

@@ -0,0 +1,329 @@
#include "sudoku.h"
#include <iostream>
#include <fstream>
#include <filesystem>
#include <algorithm>
#include <cctype>
#include <bitset>
Sudoku::Sudoku() {
clear();
}
Sudoku::Sudoku(const std::string& puzzle_str) {
clear();
loadFromString(puzzle_str);
}
bool Sudoku::loadFromString(const std::string& puzzle_str) {
if (puzzle_str.length() != 81) {
return false;
}
clear();
for (int i = 0; i < 81; ++i) {
char c = puzzle_str[i];
if (c >= '1' && c <= '9') {
int row = i / 9;
int col = i % 9;
uint8_t value = c - '0';
if (!set(row, col, value)) {
return false; // Invalid move
}
} else if (c != '0' && c != '.') {
return false; // Invalid character
}
}
return true;
}
bool Sudoku::loadFromFile(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
return false;
}
std::string line;
std::string puzzle_str;
while (std::getline(file, line)) {
// Remove whitespace and comments
line.erase(std::remove_if(line.begin(), line.end(),
[](char c) { return std::isspace(c) || c == '#'; }), line.end());
if (line.empty()) continue;
puzzle_str += line;
}
return loadFromString(puzzle_str);
}
void Sudoku::clear() {
board_.clear();
}
bool Sudoku::isValid() const {
// Check rows for duplicates using bitset for efficiency
for (int row = 0; row < 9; ++row) {
std::bitset<10> seen; // bits 1-9 track values 1-9
for (int col = 0; col < 9; ++col) {
uint8_t value = get(row, col);
if (value != 0) {
if (seen[value]) return false; // Duplicate found
seen.set(value);
}
}
}
// Check columns for duplicates
for (int col = 0; col < 9; ++col) {
std::bitset<10> seen;
for (int row = 0; row < 9; ++row) {
uint8_t value = get(row, col);
if (value != 0) {
if (seen[value]) return false; // Duplicate found
seen.set(value);
}
}
}
// Check boxes for duplicates
for (int box = 0; box < 9; ++box) {
std::bitset<10> seen;
int startRow = (box / 3) * 3;
int startCol = (box % 3) * 3;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col) {
uint8_t value = get(startRow + row, startCol + col);
if (value != 0) {
if (seen[value]) return false; // Duplicate found
seen.set(value);
}
}
}
}
return true;
}
bool Sudoku::isSolved() const {
for (int i = 0; i < 81; ++i) {
if (board_.get(i) == 0) return false;
}
return isValid();
}
void Sudoku::print() const {
for (int row = 0; row < 9; ++row) {
for (int col = 0; col < 9; ++col) {
uint8_t value = get(row, col);
std::cout << (value == 0 ? '.' : static_cast<char>('0' + value));
if (col < 8) std::cout << " ";
if (col == 2 || col == 5) std::cout << "| ";
}
std::cout << std::endl;
if (row == 2 || row == 5) {
std::cout << "------+-------+------" << std::endl;
}
}
}
std::string Sudoku::toString() const {
std::string result;
result.reserve(81);
for (int i = 0; i < 81; ++i) {
uint8_t cell = board_.get(i);
result += (cell == 0 ? '.' : static_cast<char>('0' + cell));
}
return result;
}
std::array<uint8_t, 81> Sudoku::getBoard() const {
std::array<uint8_t, 81> result;
for (int i = 0; i < 81; ++i) {
result[i] = board_.get(i);
}
return result;
}
// SudokuValidator implementation
bool SudokuValidator::isValidSolution(const std::array<uint8_t, 81>& board) {
// Check if all cells are filled and valid
for (int i = 0; i < 81; ++i) {
if (board[i] == 0) return false;
}
return isValidPartial(board);
}
bool SudokuValidator::isValidPartial(const std::array<uint8_t, 81>& board) {
return !hasConflicts(board);
}
bool SudokuValidator::hasConflicts(const std::array<uint8_t, 81>& board) {
// Check rows
for (int row = 0; row < 9; ++row) {
std::bitset<10> seen;
for (int col = 0; col < 9; ++col) {
uint8_t value = board[row * 9 + col];
if (value != 0) {
if (seen[value]) return true;
seen.set(value);
}
}
}
// Check columns
for (int col = 0; col < 9; ++col) {
std::bitset<10> seen;
for (int row = 0; row < 9; ++row) {
uint8_t value = board[row * 9 + col];
if (value != 0) {
if (seen[value]) return true;
seen.set(value);
}
}
}
// Check boxes
for (int box = 0; box < 9; ++box) {
std::bitset<10> seen;
int startRow = (box / 3) * 3;
int startCol = (box % 3) * 3;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col) {
uint8_t value = board[(startRow + row) * 9 + (startCol + col)];
if (value != 0) {
if (seen[value]) return true;
seen.set(value);
}
}
}
}
return false;
}
std::vector<std::pair<int, int>> SudokuValidator::findConflicts(const std::array<uint8_t, 81>& board) {
std::vector<std::pair<int, int>> conflicts;
// Check rows
for (int row = 0; row < 9; ++row) {
std::bitset<10> seen;
for (int col = 0; col < 9; ++col) {
uint8_t value = board[row * 9 + col];
if (value != 0) {
if (seen[value]) {
conflicts.emplace_back(row, col);
} else {
seen.set(value);
}
}
}
}
// Check columns
for (int col = 0; col < 9; ++col) {
std::bitset<10> seen;
for (int row = 0; row < 9; ++row) {
uint8_t value = board[row * 9 + col];
if (value != 0) {
if (seen[value]) {
conflicts.emplace_back(row, col);
} else {
seen.set(value);
}
}
}
}
// Check boxes
for (int box = 0; box < 9; ++box) {
std::bitset<10> seen;
int startRow = (box / 3) * 3;
int startCol = (box % 3) * 3;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col) {
int r = startRow + row;
int c = startCol + col;
uint8_t value = board[r * 9 + c];
if (value != 0) {
if (seen[value]) {
conflicts.emplace_back(r, c);
} else {
seen.set(value);
}
}
}
}
}
return conflicts;
}
// SudokuLoader implementation
std::optional<Sudoku> SudokuLoader::fromString(const std::string& puzzle_str) {
Sudoku sudoku;
if (sudoku.loadFromString(puzzle_str)) {
return sudoku;
}
return std::nullopt;
}
std::optional<Sudoku> SudokuLoader::fromFile(const std::string& filename) {
Sudoku sudoku;
if (sudoku.loadFromFile(filename)) {
return sudoku;
}
return std::nullopt;
}
std::vector<Sudoku> SudokuLoader::fromDirectory(const std::string& dirname, const std::string& extension) {
std::vector<Sudoku> puzzles;
try {
for (const auto& entry : std::filesystem::directory_iterator(dirname)) {
if (entry.is_regular_file()) {
std::string filename = entry.path().string();
if (filename.size() >= extension.size() &&
filename.substr(filename.size() - extension.size()) == extension) {
if (auto sudoku = fromFile(filename)) {
puzzles.push_back(std::move(*sudoku));
}
}
}
}
} catch (const std::filesystem::filesystem_error&) {
// Directory doesn't exist or can't be read
}
return puzzles;
}
bool SudokuLoader::parseLine(const std::string& line, std::array<uint8_t, 81>& board) {
std::string cleaned = line;
cleaned.erase(std::remove_if(cleaned.begin(), cleaned.end(),
[](char c) { return std::isspace(c); }), cleaned.end());
if (cleaned.length() != 81) return false;
for (int i = 0; i < 81; ++i) {
char c = cleaned[i];
if (c >= '1' && c <= '9') {
board[i] = c - '0';
} else if (c == '0' || c == '.') {
board[i] = 0;
} else {
return false;
}
}
return true;
}

219
demos/sudoku/sudoku.h Normal file
View File

@@ -0,0 +1,219 @@
#pragma once
#include <string>
#include <vector>
#include <optional>
#include <cstdint>
#include <array>
#include <cassert>
// 4-bit packed Sudoku board storage - optimal packing
// 81 cells * 4 bits = 324 bits
// Each byte holds 2 cells (8 bits / 4 bits per cell = 2)
// 81 cells / 2 = 40.5 bytes → 41 bytes total (with 4 bits unused)
class SudokuBoardStorage {
public:
std::array<uint8_t, 41> data;
// Get 4-bit value at position (0-80)
// Each byte contains 2 cells: [cell0(4bits)][cell1(4bits)]
// Ultra-fast: only bitwise operations, no divide/modulo!
// Optimization: pos >> 1 instead of pos / 2
// Optimization: (pos & 1) << 2 instead of (pos % 2) * 4
uint8_t get(int pos) const {
int byteIndex = pos >> 1; // pos / 2 using right shift
// Precomputed shift amounts: 4 for even positions, 0 for odd positions
// This is equivalent to: (4 - bitOffset) where bitOffset = (pos & 1) << 2
// For even pos (0,2,4,...): 4 - 0 = 4
// For odd pos (1,3,5,...): 4 - 4 = 0
int shiftAmount = 4 - ((pos & 1) << 2);
uint8_t result = (data[byteIndex] >> shiftAmount) & 0xF;
// Debug assertion: ensure result is in valid range
assert(result >= 0 && result <= 9 && "Sudoku cell value must be between 0-9");
return result;
}
// Set 4-bit value at position (0-80)
// Ultra-fast: only bitwise operations, no divide/modulo!
// Optimization: pos >> 1 instead of pos / 2
// Optimization: (pos & 1) << 2 instead of (pos % 2) * 4
void set(int pos, uint8_t value) {
// Assert that value is in valid Sudoku range (0-9)
assert(value >= 0 && value <= 9 && "Sudoku cell value must be between 0-9");
int byteIndex = pos >> 1; // pos / 2 using right shift
// Precomputed shift amounts: 4 for even positions, 0 for odd positions
int shiftAmount = 4 - ((pos & 1) << 2);
// Create mask to clear the 4 bits we're setting
uint8_t mask = ~(0xF << shiftAmount);
// Set the value (value is already 0-9, so only lower 4 bits are used)
data[byteIndex] = (data[byteIndex] & mask) | (value << shiftAmount);
}
void clear() {
data.fill(0);
}
};
// Ultra-memory-efficient Sudoku class: exactly 41 bytes
class Sudoku {
public:
Sudoku();
explicit Sudoku(const std::string& puzzle_str);
// Load from various formats
bool loadFromString(const std::string& puzzle_str);
bool loadFromFile(const std::string& filename);
// Board access (inlined for performance)
inline uint8_t get(int row, int col) const {
assert((row >= 0 && row < 9 && col >= 0 && col < 9) &&
"Sudoku::get() called with invalid position - row and col must be 0-8");
int linearIndex = getLinearIndex(row, col);
return board_.get(linearIndex);
}
inline bool set(int row, int col, uint8_t value) {
assert((row >= 0 && row < 9 && col >= 0 && col < 9) &&
"Sudoku::set() called with invalid position - row and col must be 0-8");
// Keep value validation as runtime check since it's about valid Sudoku numbers
if (value > 9) return false;
int linearIndex = getLinearIndex(row, col);
uint8_t old_value = board_.get(linearIndex);
// If same value, no change needed
if (old_value == value) return true;
// Check if new value is valid (skip for 0 as it clears)
if (value != 0 && !isValidMove(row, col, value)) {
return false;
}
board_.set(linearIndex, value);
return true;
}
void clear();
// Validation
bool isValid() const;
bool isSolved() const;
// Inlined validation (called frequently from set())
inline bool isValidMove(int row, int col, uint8_t value) const {
if (value == 0 || value > 9) return false;
return !hasRowConflictExcluding(row, col, value) &&
!hasColConflictExcluding(col, row, value) &&
!hasBoxConflictExcluding(getBoxIndex(row, col), row, col, value);
}
// Utility
void print() const;
std::string toString() const;
// Convert to standard board format for external use
std::array<uint8_t, 81> getBoard() const;
private:
SudokuBoardStorage board_;
// Helper functions (inlined for performance)
inline int getLinearIndex(int row, int col) const {
return row * 9 + col;
}
inline int getBoxIndex(int row, int col) const {
return (row / 3) * 3 + (col / 3);
}
inline bool isValidPosition(int row, int col) const {
return row >= 0 && row < 9 && col >= 0 && col < 9;
}
// Validation helpers (inlined for performance)
// Uses std::bitset<10> for efficient duplicate detection instead of arrays
inline bool hasRowConflict(int row, uint8_t value) const {
for (int col = 0; col < 9; ++col) {
if (get(row, col) == value) return true;
}
return false;
}
inline bool hasColConflict(int col, uint8_t value) const {
for (int row = 0; row < 9; ++row) {
if (get(row, col) == value) return true;
}
return false;
}
inline bool hasBoxConflict(int box, uint8_t value) const {
int startRow = (box / 3) * 3;
int startCol = (box % 3) * 3;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col) {
if (get(startRow + row, startCol + col) == value) return true;
}
}
return false;
}
// Validation helpers that exclude current position (for move validation)
inline bool hasRowConflictExcluding(int row, int excludeCol, uint8_t value) const {
for (int col = 0; col < 9; ++col) {
if (col != excludeCol && get(row, col) == value) return true;
}
return false;
}
inline bool hasColConflictExcluding(int col, int excludeRow, uint8_t value) const {
for (int row = 0; row < 9; ++row) {
if (row != excludeRow && get(row, col) == value) return true;
}
return false;
}
inline bool hasBoxConflictExcluding(int box, int excludeRow, int excludeCol, uint8_t value) const {
int startRow = (box / 3) * 3;
int startCol = (box % 3) * 3;
for (int row = 0; row < 3; ++row) {
for (int col = 0; col < 3; ++col) {
int r = startRow + row;
int c = startCol + col;
if ((r != excludeRow || c != excludeCol) && get(r, c) == value) return true;
}
}
return false;
}
};
// Static assert to ensure exactly 41 bytes
static_assert(sizeof(Sudoku) == 41, "Sudoku class must be exactly 41 bytes");
// Fast solution validator (stateless)
class SudokuValidator {
public:
static bool isValidSolution(const std::array<uint8_t, 81>& board);
static bool isValidPartial(const std::array<uint8_t, 81>& board);
static bool hasConflicts(const std::array<uint8_t, 81>& board);
static std::vector<std::pair<int, int>> findConflicts(const std::array<uint8_t, 81>& board);
};
// Fast puzzle loader
class SudokuLoader {
public:
static std::optional<Sudoku> fromString(const std::string& puzzle_str);
static std::optional<Sudoku> fromFile(const std::string& filename);
static std::vector<Sudoku> fromDirectory(const std::string& dirname, const std::string& extension = ".txt");
private:
static bool parseLine(const std::string& line, std::array<uint8_t, 81>& board);
};

View File

@@ -0,0 +1,265 @@
#include <gtest/gtest.h>
#include "sudoku.h"
// Test fixture for Sudoku tests
class SudokuTest : public ::testing::Test {
protected:
void SetUp() override {
// Common test setup
}
void TearDown() override {
// Common test cleanup
}
// Helper function to create a solved Sudoku
Sudoku createSolvedSudoku() {
Sudoku sudoku;
std::string solved = "534678912672195348198342567859761423426853791713924856961537284287419635345286179";
sudoku.loadFromString(solved);
return sudoku;
}
// Helper function to create an easy puzzle
Sudoku createEasyPuzzle() {
Sudoku sudoku;
std::string easy = "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
sudoku.loadFromString(easy);
return sudoku;
}
};
// Basic functionality tests
TEST_F(SudokuTest, EmptySudoku) {
Sudoku sudoku;
for (int row = 0; row < 9; ++row) {
for (int col = 0; col < 9; ++col) {
EXPECT_EQ(sudoku.get(row, col), 0);
}
}
}
TEST_F(SudokuTest, SetAndGet) {
Sudoku sudoku;
// Test setting and getting values
sudoku.set(0, 0, 5);
EXPECT_EQ(sudoku.get(0, 0), 5);
sudoku.set(8, 8, 9);
EXPECT_EQ(sudoku.get(8, 8), 9);
sudoku.set(4, 4, 7);
EXPECT_EQ(sudoku.get(4, 4), 7);
}
TEST_F(SudokuTest, LoadFromString) {
Sudoku sudoku;
std::string puzzle = "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
EXPECT_TRUE(sudoku.loadFromString(puzzle));
// Verify specific known values
EXPECT_EQ(sudoku.get(0, 0), 5);
EXPECT_EQ(sudoku.get(0, 1), 3);
EXPECT_EQ(sudoku.get(0, 6), 0); // Empty cell
}
TEST_F(SudokuTest, LoadInvalidString) {
Sudoku sudoku;
// Test with string that's too short
EXPECT_FALSE(sudoku.loadFromString("123"));
// Test with invalid characters
EXPECT_FALSE(sudoku.loadFromString("53007000060019500009800006080006000340080300170002000606000028000041900500008007a"));
}
TEST_F(SudokuTest, Clear) {
Sudoku sudoku;
sudoku.set(0, 0, 5);
sudoku.set(1, 1, 3);
sudoku.set(2, 2, 7);
EXPECT_EQ(sudoku.get(0, 0), 5);
EXPECT_EQ(sudoku.get(1, 1), 3);
EXPECT_EQ(sudoku.get(2, 2), 7);
sudoku.clear();
for (int row = 0; row < 9; ++row) {
for (int col = 0; col < 9; ++col) {
EXPECT_EQ(sudoku.get(row, col), 0);
}
}
}
TEST_F(SudokuTest, MemorySize) {
EXPECT_EQ(sizeof(Sudoku), 41);
}
TEST_F(SudokuTest, SetInvalidValue) {
Sudoku sudoku;
// Test setting value > 9
EXPECT_FALSE(sudoku.set(0, 0, 10));
EXPECT_FALSE(sudoku.set(0, 0, 15));
EXPECT_FALSE(sudoku.set(0, 0, 255));
// Valid values should work
EXPECT_TRUE(sudoku.set(0, 0, 9));
EXPECT_EQ(sudoku.get(0, 0), 9);
}
// Validation tests
TEST_F(SudokuTest, ValidMoves) {
auto sudoku = createEasyPuzzle();
// Test valid moves - place numbers where there are empty cells
EXPECT_TRUE(sudoku.set(0, 2, 1)); // Should be valid - empty cell
EXPECT_EQ(sudoku.get(0, 2), 1);
EXPECT_TRUE(sudoku.set(1, 1, 4)); // Should be valid - empty cell
EXPECT_EQ(sudoku.get(1, 1), 4);
EXPECT_TRUE(sudoku.set(2, 0, 2)); // Should be valid - empty cell, different value
EXPECT_EQ(sudoku.get(2, 0), 2);
}
TEST_F(SudokuTest, InvalidMoves) {
auto sudoku = createEasyPuzzle();
// Test moves that conflict with existing numbers
EXPECT_FALSE(sudoku.set(0, 0, 6)); // Conflicts with existing 5 in row
EXPECT_FALSE(sudoku.set(0, 1, 6)); // Conflicts with existing 3 in column (try different value)
EXPECT_FALSE(sudoku.set(2, 2, 9)); // Conflicts with existing 8 in same box
// Test setting same value as existing (should work)
EXPECT_TRUE(sudoku.set(0, 0, 5)); // Same as existing value
EXPECT_EQ(sudoku.get(0, 0), 5);
EXPECT_TRUE(sudoku.set(0, 1, 3)); // Same as existing value
EXPECT_EQ(sudoku.get(0, 1), 3);
}
TEST_F(SudokuTest, SolvedPuzzle) {
auto sudoku = createSolvedSudoku();
EXPECT_TRUE(sudoku.isValid());
EXPECT_TRUE(sudoku.isSolved());
}
TEST_F(SudokuTest, PartialPuzzle) {
auto sudoku = createEasyPuzzle();
EXPECT_TRUE(sudoku.isValid());
EXPECT_FALSE(sudoku.isSolved());
}
// Board conversion tests
TEST_F(SudokuTest, GetBoard) {
auto sudoku = createEasyPuzzle();
auto board = sudoku.getBoard();
EXPECT_EQ(board.size(), 81);
EXPECT_EQ(board[0], 5); // First cell
EXPECT_EQ(board[1], 3); // Second cell
}
TEST_F(SudokuTest, ToString) {
Sudoku sudoku;
sudoku.set(0, 0, 5);
sudoku.set(0, 1, 3);
std::string str = sudoku.toString();
EXPECT_EQ(str.length(), 81);
EXPECT_EQ(str[0], '5');
EXPECT_EQ(str[1], '3');
}
// Validator tests
TEST_F(SudokuTest, ValidatorValidSolution) {
auto sudoku = createSolvedSudoku();
auto board = sudoku.getBoard();
EXPECT_TRUE(SudokuValidator::isValidSolution(board));
EXPECT_TRUE(SudokuValidator::isValidPartial(board));
EXPECT_FALSE(SudokuValidator::hasConflicts(board));
}
TEST_F(SudokuTest, ValidatorInvalidSolution) {
auto sudoku = createSolvedSudoku();
auto board = sudoku.getBoard();
// Create a conflict
board[1] = 5; // This creates duplicate 5 in first row
EXPECT_FALSE(SudokuValidator::isValidSolution(board));
EXPECT_FALSE(SudokuValidator::isValidPartial(board));
EXPECT_TRUE(SudokuValidator::hasConflicts(board));
auto conflicts = SudokuValidator::findConflicts(board);
EXPECT_GT(conflicts.size(), 0);
}
// Performance tests
TEST_F(SudokuTest, PerformanceGetOperations) {
auto sudoku = createEasyPuzzle();
// Time 100,000 get operations
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100000; ++i) {
int row = i % 9;
int col = (i / 9) % 9;
volatile uint8_t value = sudoku.get(row, col);
(void)value; // Prevent optimization
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "100,000 get operations took: " << duration.count() << " microseconds" << std::endl;
std::cout << "Average per operation: " << (duration.count() / 100000.0) << " microseconds" << std::endl;
}
TEST_F(SudokuTest, PerformanceSetOperations) {
Sudoku sudoku;
// Time 100,000 set operations
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100000; ++i) {
int row = i % 9;
int col = (i / 9) % 9;
int value = (i % 9) + 1;
sudoku.set(row, col, value);
}
auto end = std::chrono::high_resolution_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "100,000 set operations took: " << duration.count() << " microseconds" << std::endl;
std::cout << "Average per operation: " << (duration.count() / 100000.0) << " microseconds" << std::endl;
}
// Edge case tests
TEST_F(SudokuTest, EdgeCases) {
Sudoku sudoku;
// Test all edge positions
EXPECT_TRUE(sudoku.set(0, 0, 1)); // Top-left
EXPECT_TRUE(sudoku.set(0, 8, 2)); // Top-right
EXPECT_TRUE(sudoku.set(8, 0, 3)); // Bottom-left
EXPECT_TRUE(sudoku.set(8, 8, 4)); // Bottom-right
EXPECT_EQ(sudoku.get(0, 0), 1);
EXPECT_EQ(sudoku.get(0, 8), 2);
EXPECT_EQ(sudoku.get(8, 0), 3);
EXPECT_EQ(sudoku.get(8, 8), 4);
}
int main(int argc, char **argv) {
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}

View File

@@ -1,8 +1,5 @@
set(EXAMPLE_SOURCES
basic_wfc_2d.cpp
basic_wfc_3d.cpp
texture_generation.cpp
model_generation.cpp
)
# Create executables for each example

View File

@@ -0,0 +1,125 @@
#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

174
include/nd-wfc/entropy.hpp Normal file
View File

@@ -0,0 +1,174 @@
#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

146
include/nd-wfc/grid.hpp Normal file
View File

@@ -0,0 +1,146 @@
#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

@@ -0,0 +1,185 @@
#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

142
include/nd-wfc/types.hpp Normal file
View File

@@ -0,0 +1,142 @@
#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

276
include/nd-wfc/wave.hpp Normal file
View File

@@ -0,0 +1,276 @@
#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

186
include/nd-wfc/wfc.hpp Normal file
View File

@@ -0,0 +1,186 @@
#pragma once
#include "types.hpp"
#include "grid.hpp"
#include "wave.hpp"
#include "constraint.hpp"
#include "entropy.hpp"
#include "propagator.hpp"
#include <random>
#include <chrono>
namespace nd_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);
}
// 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
}
}
}
};
// Type aliases for common dimensions
using Wfc2D = Wfc<2>;
using Wfc3D = Wfc<3>;
using Wfc4D = Wfc<4>;
} // namespace nd_wfc

3
prompts/1-2DSimple Normal file
View File

@@ -0,0 +1,3 @@
This is the start of a high performant c++ Wave Function Collapse repo for n-dimensions. To increase performance we want to avoid using a large memory footprint, virtual polymoprhism, etc.
For now we want to make it possible to use and test WFC with 2 dimensions, but later on we'll want to use 3D or even 4D. Please implement the algorithm with 2 dimensions for now. use the console for outputting tests.

1
prompts/2-sudoku Normal file
View File

@@ -0,0 +1 @@
Can you make a fast sudoku loader and solution validator in demos/sudoku/. make sure to use as little memory as possible for the sudoku class and validator

View File

@@ -1,21 +1,20 @@
set(SOURCES
# Create interface library (header-only)
add_library(nd-wfc INTERFACE)
)
# Create main executable
add_executable(nd-wfc-main main.cpp)
set(HEADERS
)
# Create library
add_library(nd-wfc ${SOURCES} ${HEADERS})
# Include directories
# Include directories for interface library
target_include_directories(nd-wfc
PUBLIC
INTERFACE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/../include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
)
# Include directories for main executable
target_include_directories(nd-wfc-main
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/../include
)
# Link external dependencies

View File

@@ -1,6 +1,248 @@
#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,55 +1,63 @@
# RapidJSON - header-only library
if(ND_WFC_USE_SYSTEM_LIBS)
find_package(RapidJSON REQUIRED)
else()
# Use the git submodule
# Disable examples and tests to avoid compilation issues with newer GCC
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)
# Third-party dependencies - optional components for future features
# For now, we'll make these optional since the core WFC doesn't need them
# Create a target for RapidJSON if not already created
if(NOT TARGET rapidjson)
add_library(rapidjson INTERFACE)
target_include_directories(rapidjson
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/rapidjson/include
)
# 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
# Assimp - 3D model loading (optional)
if(ND_WFC_USE_SYSTEM_LIBS)
find_package(assimp REQUIRED)
find_package(assimp QUIET)
if(assimp_FOUND)
message(STATUS "Found system Assimp")
endif()
else()
# Use the git submodule
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)
# Assimp target should be created by the assimp CMakeLists.txt
# 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
# SDL3 - for graphics and windowing (optional)
if(ND_WFC_USE_SYSTEM_LIBS)
find_package(SDL3 REQUIRED)
find_package(SDL3 QUIET)
if(SDL3_FOUND)
message(STATUS "Found system SDL3")
endif()
else()
# Use the git submodule
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)
set(SDL_VIDEO OFF CACHE BOOL "Enable video subsystem" FORCE) # Disable video for headless builds
set(SDL_GPU OFF CACHE BOOL "Enable GPU subsystem" FORCE)
set(SDL_RENDER OFF CACHE BOOL "Enable render subsystem" FORCE)
set(SDL_CAMERA OFF CACHE BOOL "Enable camera subsystem" FORCE)
set(SDL_UNIX_CONSOLE_BUILD ON CACHE BOOL "Build SDL for console (headless) applications" FORCE)
add_subdirectory(SDL)
# SDL3 target should be created by the SDL CMakeLists.txt
# 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)