Merge commit '2616eaf57a1c3facda4e7b11db092e03b0b652ac'
This commit is contained in:
@@ -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
105
demos/sudoku/CMakeLists.txt
Normal 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
316
demos/sudoku/README.md
Normal 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.
|
||||
225
demos/sudoku/benchmark_sudoku.cpp
Normal file
225
demos/sudoku/benchmark_sudoku.cpp
Normal 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
93
demos/sudoku/build.sh
Executable 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
|
||||
119681
demos/sudoku/data/Sudoku_diabolical.txt
Normal file
119681
demos/sudoku/data/Sudoku_diabolical.txt
Normal file
File diff suppressed because it is too large
Load Diff
452643
demos/sudoku/data/Sudoku_easy.txt
Normal file
452643
demos/sudoku/data/Sudoku_easy.txt
Normal file
File diff suppressed because it is too large
Load Diff
321592
demos/sudoku/data/Sudoku_hard.txt
Normal file
321592
demos/sudoku/data/Sudoku_hard.txt
Normal file
File diff suppressed because it is too large
Load Diff
352643
demos/sudoku/data/Sudoku_medium.txt
Normal file
352643
demos/sudoku/data/Sudoku_medium.txt
Normal file
File diff suppressed because it is too large
Load Diff
329
demos/sudoku/sudoku.cpp
Normal file
329
demos/sudoku/sudoku.cpp
Normal 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
219
demos/sudoku/sudoku.h
Normal 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);
|
||||
};
|
||||
265
demos/sudoku/test_sudoku.cpp
Normal file
265
demos/sudoku/test_sudoku.cpp
Normal 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();
|
||||
}
|
||||
@@ -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
|
||||
|
||||
125
include/nd-wfc/constraint.hpp
Normal file
125
include/nd-wfc/constraint.hpp
Normal 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
174
include/nd-wfc/entropy.hpp
Normal 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
146
include/nd-wfc/grid.hpp
Normal 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
|
||||
185
include/nd-wfc/propagator.hpp
Normal file
185
include/nd-wfc/propagator.hpp
Normal 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
142
include/nd-wfc/types.hpp
Normal 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
276
include/nd-wfc/wave.hpp
Normal 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
186
include/nd-wfc/wfc.hpp
Normal 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
3
prompts/1-2DSimple
Normal 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
1
prompts/2-sudoku
Normal 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
|
||||
@@ -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
|
||||
|
||||
242
src/main.cpp
242
src/main.cpp
@@ -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;
|
||||
}
|
||||
92
thirdparty/CMakeLists.txt
vendored
92
thirdparty/CMakeLists.txt
vendored
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user