From 51bda8ddb8d5d85d498e70392bd5a297e78200b2 Mon Sep 17 00:00:00 2001 From: cdemeyer-teachx Date: Mon, 25 Aug 2025 13:08:32 +0900 Subject: [PATCH] failure analysis framework --- .gitignore | 1 + demos/sudoku/CMakeLists.txt | 44 +++- demos/sudoku/analyze_failing_puzzles.cpp | 190 ++++++++++++++++ demos/sudoku/debug_failing_puzzles.cpp | 196 +++++++++++++++++ demos/sudoku/sudoku.cpp | 2 +- demos/sudoku/sudoku.h | 23 +- demos/sudoku/sudoku_wfc.cpp | 1 - demos/sudoku/test_sudoku.cpp | 269 ++++++++++++++++++++++- include/nd-wfc/wfc.hpp | 10 +- 9 files changed, 704 insertions(+), 32 deletions(-) create mode 100644 demos/sudoku/analyze_failing_puzzles.cpp create mode 100644 demos/sudoku/debug_failing_puzzles.cpp diff --git a/.gitignore b/.gitignore index 05237a4..ac5b513 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,4 @@ thirdparty/*/install/ # Temporary files *.tmp *.temp +demos/sudoku/data/Sudoku_failing.txt diff --git a/demos/sudoku/CMakeLists.txt b/demos/sudoku/CMakeLists.txt index d8e88d2..e2606a8 100644 --- a/demos/sudoku/CMakeLists.txt +++ b/demos/sudoku/CMakeLists.txt @@ -45,6 +45,18 @@ add_executable(sudoku_wfc_demo sudoku.cpp ) +# Create failing puzzles analyzer executable +add_executable(analyze_failing_puzzles + analyze_failing_puzzles.cpp + sudoku.cpp +) + +# Create debug failing puzzles executable +add_executable(debug_failing_puzzles + debug_failing_puzzles.cpp + sudoku.cpp +) + # Set output directory for sudoku_demo set_target_properties(sudoku_demo PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin @@ -57,21 +69,47 @@ set_target_properties(sudoku_wfc_demo PROPERTIES CXX_STANDARD_REQUIRED ON ) +# Set output directory and properties for analyze_failing_puzzles +set_target_properties(analyze_failing_puzzles PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON +) + +# Set output directory and properties for debug_failing_puzzles +set_target_properties(debug_failing_puzzles PROPERTIES + RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON +) + # Include directories target_include_directories(sudoku_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(sudoku_wfc_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../../include ) +target_include_directories(analyze_failing_puzzles PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../include +) +target_include_directories(debug_failing_puzzles PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/../../include +) # Optional: Enable optimizations for release builds if(CMAKE_BUILD_TYPE STREQUAL "Release") if(MSVC) target_compile_options(sudoku_demo PRIVATE /O2) target_compile_options(sudoku_wfc_demo PRIVATE /O2) + target_compile_options(analyze_failing_puzzles PRIVATE /O2) + target_compile_options(debug_failing_puzzles PRIVATE /O2) else() target_compile_options(sudoku_demo PRIVATE -O3 -march=native) target_compile_options(sudoku_wfc_demo PRIVATE -O3 -march=native) + target_compile_options(analyze_failing_puzzles PRIVATE -O3 -march=native) + target_compile_options(debug_failing_puzzles PRIVATE -O3 -march=native) endif() endif() @@ -126,9 +164,9 @@ endif() # Installation (optional) if(HAS_GTEST AND HAS_BENCHMARK) - install(TARGETS sudoku_demo sudoku_wfc_demo sudoku_tests sudoku_benchmarks DESTINATION bin) + install(TARGETS sudoku_demo sudoku_wfc_demo analyze_failing_puzzles debug_failing_puzzles sudoku_tests sudoku_benchmarks DESTINATION bin) elseif(HAS_GTEST) - install(TARGETS sudoku_demo sudoku_wfc_demo sudoku_tests DESTINATION bin) + install(TARGETS sudoku_demo sudoku_wfc_demo analyze_failing_puzzles debug_failing_puzzles sudoku_tests DESTINATION bin) else() - install(TARGETS sudoku_demo sudoku_wfc_demo DESTINATION bin) + install(TARGETS sudoku_demo sudoku_wfc_demo analyze_failing_puzzles debug_failing_puzzles DESTINATION bin) endif() diff --git a/demos/sudoku/analyze_failing_puzzles.cpp b/demos/sudoku/analyze_failing_puzzles.cpp new file mode 100644 index 0000000..bdc4fa2 --- /dev/null +++ b/demos/sudoku/analyze_failing_puzzles.cpp @@ -0,0 +1,190 @@ +#include "sudoku.h" +#include +#include +#include +#include +#include +#include +#include + +// Helper function to load multiple puzzles from a file (one puzzle per line) +std::vector loadPuzzlesFromFile(const std::string& filename) { + std::vector puzzles; + std::ifstream file(filename); + + if (!file.is_open()) { + std::cerr << "Failed to open file: " << filename << std::endl; + return puzzles; + } + + std::string line; + while (std::getline(file, line)) { + // Remove whitespace + line.erase(std::remove_if(line.begin(), line.end(), + [](char c) { return std::isspace(c); }), line.end()); + + if (line.empty()) continue; + + Sudoku sudoku; + if (sudoku.loadFromString(line)) { + puzzles.push_back(std::move(sudoku)); + } + } + + return puzzles; +} + +// Helper function to save failing puzzles to a file +void saveFailingPuzzles(const std::vector& failingPuzzles, const std::string& outputFile) { + std::ofstream file(outputFile); + if (!file.is_open()) { + std::cerr << "Failed to open output file: " << outputFile << std::endl; + return; + } + + for (const auto& puzzle : failingPuzzles) { + file << puzzle << std::endl; + } + + std::cout << "Saved " << failingPuzzles.size() << " failing puzzles to " << outputFile << std::endl; +} + +int main() { + // Create WFC solver + auto sudokuSolver = WFC::Builder() + .DefineIDs<1, 2, 3, 4, 5, 6, 7, 8, 9>() + .Variable<1, 2, 3, 4, 5, 6, 7, 8, 9>([](Sudoku&, size_t index, WFC::WorldValue val, auto& constrainer) { + size_t x = index % 9; + size_t y = index / 9; + + // Add row constraints (same row, different columns) + for (size_t i = 0; i < 9; ++i) { + if (i != x) constrainer.Exclude(val, i + y * 9); + } + + // Add column constraints (same column, different rows) + for (size_t i = 0; i < 9; ++i) { + if (i != y) constrainer.Exclude(val,x + i * 9); + } + + // Add box constraints (3x3 box) + size_t box_x = (x / 3) * 3; + size_t box_y = (y / 3) * 3; + for (size_t j = 0; j < 3; ++j) { + for (size_t k = 0; k < 3; ++k) { + size_t cell_x = box_x + j; + size_t cell_y = box_y + k; + size_t cell_index = cell_x + cell_y * 9; + if (cell_index != index) { + constrainer.Exclude(val, cell_index); + } + } + } + }) + .build(); + + // File paths + const std::string dataPath = "/home/connor/repos/nd-wfc/demos/sudoku/data"; + const std::vector inputFiles = { + dataPath + "/Sudoku_easy.txt", + dataPath + "/Sudoku_medium.txt", + dataPath + "/Sudoku_hard.txt", + dataPath + "/Sudoku_diabolical.txt" + }; + const std::string outputFile = dataPath + "/Sudoku_failing.txt"; + + // Collect all failing puzzles + std::vector allFailingPuzzles; + std::vector> failureStats; // filename -> count + + std::cout << "Analyzing Sudoku puzzles for solver failures..." << std::endl; + std::cout << "=================================================" << std::endl; + + for (const auto& inputFile : inputFiles) { + std::cout << "\nProcessing " << inputFile << "..." << std::endl; + + // Load puzzles from file + auto puzzles = loadPuzzlesFromFile(inputFile); + if (puzzles.empty()) { + std::cout << "No puzzles loaded from " << inputFile << std::endl; + continue; + } + + std::cout << "Loaded " << puzzles.size() << " puzzles" << std::endl; + + // Test each puzzle + int solvedCount = 0; + int failedCount = 0; + std::vector failingPuzzles; + + auto start = std::chrono::high_resolution_clock::now(); + + for (size_t i = 0; i < puzzles.size(); ++i) { + auto& sudoku = puzzles[i]; + + // Validate puzzle before solving + if (!sudoku.isValid()) { + std::cout << "Puzzle " << i << " is invalid, skipping" << std::endl; + failedCount++; + continue; + } + + // Try to solve + auto puzzleStart = std::chrono::high_resolution_clock::now(); + sudokuSolver.Run(sudoku, false); // false = disable verbose output for speed + auto puzzleEnd = std::chrono::high_resolution_clock::now(); + + bool solved = sudoku.isSolved(); + + if (solved) { + solvedCount++; + } else { + failedCount++; + // Get the original puzzle string for saving + std::string puzzleStr = sudoku.toString(); + failingPuzzles.push_back(puzzleStr); + + auto duration = std::chrono::duration_cast(puzzleEnd - puzzleStart); + std::cout << "Puzzle " << i << " failed to solve in " << duration.count() << "ms" << std::endl; + } + + // Progress indicator for large files + if ((i + 1) % 1000 == 0) { + std::cout << "Progress: " << (i + 1) << "/" << puzzles.size() + << " (" << solvedCount << " solved, " << failedCount << " failed)" << std::endl; + } + } + + auto end = std::chrono::high_resolution_clock::now(); + auto totalDuration = std::chrono::duration_cast(end - start); + + std::cout << "\nResults for " << inputFile << ":" << std::endl; + std::cout << " Total puzzles in file: " << puzzles.size() << std::endl; + std::cout << " Solved: " << solvedCount << std::endl; + std::cout << " Failed: " << failedCount << std::endl; + std::cout << " Success rate: " << (puzzles.size() > 0 ? (solvedCount * 100.0 / puzzles.size()) : 0) << "%" << std::endl; + std::cout << " Total time: " << totalDuration.count() << " seconds" << std::endl; + + // Add failing puzzles to the global list + allFailingPuzzles.insert(allFailingPuzzles.end(), failingPuzzles.begin(), failingPuzzles.end()); + failureStats.push_back({inputFile, failedCount}); + } + + // Save all failing puzzles to output file + std::cout << "\n=================================================" << std::endl; + std::cout << "Analysis complete!" << std::endl; + std::cout << "Total failing puzzles across all files: " << allFailingPuzzles.size() << std::endl; + + if (!allFailingPuzzles.empty()) { + saveFailingPuzzles(allFailingPuzzles, outputFile); + + std::cout << "\nFailure statistics by file:" << std::endl; + for (const auto& stat : failureStats) { + std::cout << " " << stat.first << ": " << stat.second << " failures" << std::endl; + } + } else { + std::cout << "No failing puzzles found!" << std::endl; + } + + return 0; +} diff --git a/demos/sudoku/debug_failing_puzzles.cpp b/demos/sudoku/debug_failing_puzzles.cpp new file mode 100644 index 0000000..973a8b6 --- /dev/null +++ b/demos/sudoku/debug_failing_puzzles.cpp @@ -0,0 +1,196 @@ +#include "sudoku.h" +#include +#include +#include +#include +#include +#include +#include +#include + +// Helper function to load puzzles from a file (one puzzle per line) +std::vector loadPuzzlesFromFile(const std::string& filename) { + std::vector puzzles; + std::ifstream file(filename); + + if (!file.is_open()) { + std::cerr << "Failed to open file: " << filename << std::endl; + return puzzles; + } + + std::string line; + while (std::getline(file, line)) { + // Remove whitespace + line.erase(std::remove_if(line.begin(), line.end(), + [](char c) { return std::isspace(c); }), line.end()); + + if (line.empty()) continue; + + Sudoku sudoku; + if (sudoku.loadFromString(line)) { + puzzles.push_back(std::move(sudoku)); + } + } + + return puzzles; +} + +// Helper function to print a puzzle in a nice format +void printPuzzle(const Sudoku& sudoku, const std::string& title = "") { + if (!title.empty()) { + std::cout << title << std::endl; + std::cout << std::string(title.length(), '=') << std::endl; + } + + for (int row = 0; row < 9; ++row) { + for (int col = 0; col < 9; ++col) { + uint8_t value = sudoku.get(row, col); + std::cout << (value == 0 ? '.' : static_cast('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::cout << std::endl; +} + +// Helper function to count filled cells in a puzzle +int countFilledCells(const Sudoku& sudoku) { + int count = 0; + for (int i = 0; i < 81; ++i) { + if (sudoku.get(i / 9, i % 9) != 0) { + count++; + } + } + return count; +} + +// Analyze a single failing puzzle in detail +void analyzeFailingPuzzle(const Sudoku& originalPuzzle, int puzzleIndex) { + std::cout << "\n" << std::string(60, '=') << std::endl; + std::cout << "ANALYZING FAILING PUZZLE #" << puzzleIndex << std::endl; + std::cout << std::string(60, '=') << std::endl; + + // Show original puzzle + printPuzzle(originalPuzzle, "Original Puzzle"); + + // Show statistics + int filledCells = countFilledCells(originalPuzzle); + std::cout << "Statistics:" << std::endl; + std::cout << " Filled cells: " << filledCells << "/81 (" << std::fixed << std::setprecision(1) + << (filledCells * 100.0 / 81) << "%)" << std::endl; + std::cout << " Empty cells: " << (81 - filledCells) << std::endl; + std::cout << " Is valid: " << (originalPuzzle.isValid() ? "Yes" : "No") << std::endl; + std::cout << " Is solved: " << (originalPuzzle.isSolved() ? "Yes" : "No") << std::endl; + std::cout << std::endl; + + // Try different solving approaches + auto sudokuSolver = WFC::Builder() + .DefineIDs<1, 2, 3, 4, 5, 6, 7, 8, 9>() + .Variable<1, 2, 3, 4, 5, 6, 7, 8, 9>([](Sudoku&, size_t index, WFC::WorldValue val, auto& constrainer) { + size_t x = index % 9; + size_t y = index / 9; + + // Add row constraints (same row, different columns) + for (size_t i = 0; i < 9; ++i) { + if (i != x) constrainer.Exclude(val, i + y * 9); + } + + // Add column constraints (same column, different rows) + for (size_t i = 0; i < 9; ++i) { + if (i != y) constrainer.Exclude(val,x + i * 9); + } + + // Add box constraints (3x3 box) + size_t box_x = (x / 3) * 3; + size_t box_y = (y / 3) * 3; + for (size_t j = 0; j < 3; ++j) { + for (size_t k = 0; k < 3; ++k) { + size_t cell_x = box_x + j; + size_t cell_y = box_y + k; + size_t cell_index = cell_x + cell_y * 9; + if (cell_index != index) { + constrainer.Exclude(val, cell_index); + } + } + } + }) + .build(); + + // Test 1: Try solving with different configurations + std::cout << "Testing different solving approaches:" << std::endl; + + // Test with verbose output to see what happens + Sudoku testPuzzle = originalPuzzle; + std::cout << "\nTest 1: Solving with verbose output..." << std::endl; + + auto start = std::chrono::high_resolution_clock::now(); + sudokuSolver.Run(testPuzzle, true); // Enable verbose output + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + + bool solved = testPuzzle.isSolved(); + std::cout << "Result: " << (solved ? "SOLVED" : "FAILED") << std::endl; + std::cout << "Time taken: " << duration.count() << "ms" << std::endl; + + if (solved) { + printPuzzle(testPuzzle, "Solved Puzzle"); + } + + // Test 2: Multiple attempts + if (!solved) { + std::cout << "\nTest 2: Multiple solving attempts..." << std::endl; + + int attempts = 3; + for (int attempt = 1; attempt <= attempts; ++attempt) { + Sudoku attemptPuzzle = originalPuzzle; + std::cout << "Attempt " << attempt << ": "; + + auto attemptStart = std::chrono::high_resolution_clock::now(); + sudokuSolver.Run(attemptPuzzle, false); // No verbose output + auto attemptEnd = std::chrono::high_resolution_clock::now(); + auto attemptDuration = std::chrono::duration_cast(attemptEnd - attemptStart); + + bool attemptSolved = attemptPuzzle.isSolved(); + std::cout << (attemptSolved ? "SOLVED" : "FAILED") + << " (" << attemptDuration.count() << "ms)" << std::endl; + + if (attemptSolved) { + std::cout << "SUCCESS on attempt " << attempt << "!" << std::endl; + printPuzzle(attemptPuzzle, "Successfully Solved Puzzle"); + break; + } + } + } +} + +int main() { + const std::string failingPuzzlesFile = "/home/connor/repos/nd-wfc/demos/sudoku/data/Sudoku_failing.txt"; + + std::cout << "Loading failing puzzles from: " << failingPuzzlesFile << std::endl; + + // Load failing puzzles + auto failingPuzzles = loadPuzzlesFromFile(failingPuzzlesFile); + + if (failingPuzzles.empty()) { + std::cout << "No failing puzzles found!" << std::endl; + return 0; + } + + std::cout << "Found " << failingPuzzles.size() << " failing puzzles" << std::endl; + + // Analyze each failing puzzle + for (size_t i = 0; i < failingPuzzles.size(); ++i) { + analyzeFailingPuzzle(failingPuzzles[i], i + 1); + } + + std::cout << "\n" << std::string(60, '=') << std::endl; + std::cout << "ANALYSIS COMPLETE" << std::endl; + std::cout << "Total failing puzzles analyzed: " << failingPuzzles.size() << std::endl; + std::cout << std::string(60, '=') << std::endl; + + return 0; +} diff --git a/demos/sudoku/sudoku.cpp b/demos/sudoku/sudoku.cpp index 6b96d8e..045b3ba 100644 --- a/demos/sudoku/sudoku.cpp +++ b/demos/sudoku/sudoku.cpp @@ -326,4 +326,4 @@ bool SudokuLoader::parseLine(const std::string& line, std::array& b } return true; -} +} \ No newline at end of file diff --git a/demos/sudoku/sudoku.h b/demos/sudoku/sudoku.h index bb1a919..d2fe1c8 100644 --- a/demos/sudoku/sudoku.h +++ b/demos/sudoku/sudoku.h @@ -209,31 +209,10 @@ public: // WFC Support constexpr size_t size() const { return 81; } - -public: // Solver Interface - // Solve the puzzle using WFC algorithm - bool solve(); - - // Solve with custom initial constraints (for testing) - bool solveWithConstraints(const std::vector>& constraints); - - // Get the number of attempts made during solving - int getSolveAttempts() const { return solve_attempts_; } - - // Get the time taken for the last solve operation (in microseconds) - long long getSolveTimeMicroseconds() const { return solve_time_us_; } - -private: - mutable int solve_attempts_ = 0; - mutable long long solve_time_us_ = 0; - - // Helper method for backtracking solver - bool solveBacktracking(size_t index); - }; // Static assert to ensure correct size (now 56 bytes with solver additions) -static_assert(sizeof(Sudoku) == 56, "Sudoku class must be exactly 56 bytes"); +static_assert(sizeof(Sudoku) == 41, "Sudoku class must be exactly 41 bytes"); // Fast solution validator (stateless) class SudokuValidator { diff --git a/demos/sudoku/sudoku_wfc.cpp b/demos/sudoku/sudoku_wfc.cpp index dac5f39..9a50a63 100644 --- a/demos/sudoku/sudoku_wfc.cpp +++ b/demos/sudoku/sudoku_wfc.cpp @@ -1,5 +1,4 @@ #include -#include #include "sudoku.h" #include diff --git a/demos/sudoku/test_sudoku.cpp b/demos/sudoku/test_sudoku.cpp index 6beca34..dff7777 100644 --- a/demos/sudoku/test_sudoku.cpp +++ b/demos/sudoku/test_sudoku.cpp @@ -1,10 +1,49 @@ #include #include "sudoku.h" #include +#include +#include +#include + +// Forward declaration for helper function +std::vector loadPuzzlesFromFile(const std::string& filename); + +static auto sudokuTestSolver = WFC::Builder() + .DefineIDs<1, 2, 3, 4, 5, 6, 7, 8, 9>() + .Variable<1, 2, 3, 4, 5, 6, 7, 8, 9>([](Sudoku&, size_t index, WFC::WorldValue val, auto& constrainer) { + size_t x = index % 9; + size_t y = index / 9; + + // Add row constraints (same row, different columns) + for (size_t i = 0; i < 9; ++i) { + if (i != x) constrainer.Exclude(val, i + y * 9); + } + + // Add column constraints (same column, different rows) + for (size_t i = 0; i < 9; ++i) { + if (i != y) constrainer.Exclude(val,x + i * 9); + } + + // Add box constraints (3x3 box) + size_t box_x = (x / 3) * 3; + size_t box_y = (y / 3) * 3; + for (size_t j = 0; j < 3; ++j) { + for (size_t k = 0; k < 3; ++k) { + size_t cell_x = box_x + j; + size_t cell_y = box_y + k; + size_t cell_index = cell_x + cell_y * 9; + if (cell_index != index) { + constrainer.Exclude(val, cell_index); + } + } + } + }) + .build(); // Test fixture for Sudoku tests class SudokuTest : public ::testing::Test { protected: + void SetUp() override { // Common test setup } @@ -28,6 +67,11 @@ protected: sudoku.loadFromString(easy); return sudoku; } + + Sudoku SolvePuzzle(Sudoku& sudoku) { + sudokuTestSolver.Run(sudoku, true); + return sudoku; + } }; // Basic functionality tests @@ -97,7 +141,7 @@ TEST_F(SudokuTest, Clear) { } TEST_F(SudokuTest, MemorySize) { - EXPECT_EQ(sizeof(Sudoku), 56); // Updated to include solver members + EXPECT_EQ(sizeof(Sudoku), 41); } TEST_F(SudokuTest, SetInvalidValue) { @@ -260,6 +304,229 @@ TEST_F(SudokuTest, EdgeCases) { EXPECT_EQ(sudoku.get(8, 8), 4); } +TEST_F(SudokuTest, WFCIntegration) +{ + auto sudoku = createEasyPuzzle(); + sudokuTestSolver.Run(sudoku, true); + EXPECT_TRUE(sudoku.isSolved()); +} + +// Tests loading and solving puzzles from data files +TEST_F(SudokuTest, LoadAndSolveEasyPuzzles) +{ + std::vector easyPuzzles = loadPuzzlesFromFile("/home/connor/repos/nd-wfc/demos/sudoku/data/Sudoku_easy.txt"); + + ASSERT_GT(easyPuzzles.size(), 0) << "No easy puzzles loaded"; + + int solvedCount = 0; + auto start = std::chrono::high_resolution_clock::now(); + + for (size_t i = 0; i < easyPuzzles.size(); ++i) { + auto& sudoku = easyPuzzles[i]; + EXPECT_TRUE(sudoku.isValid()) << "Puzzle " << i << " is not valid"; + + auto puzzleStart = std::chrono::high_resolution_clock::now(); + sudokuTestSolver.Run(sudoku, true); + auto puzzleEnd = std::chrono::high_resolution_clock::now(); + + EXPECT_TRUE(sudoku.isSolved()) << "Puzzle " << i << " was not solved"; + + if (sudoku.isSolved()) { + solvedCount++; + } + + auto puzzleDuration = std::chrono::duration_cast(puzzleEnd - puzzleStart); + if (i < 5) { // Only print timing for first 5 puzzles to avoid spam + std::cout << "Easy puzzle " << i << " solved in " << puzzleDuration.count() << "ms" << std::endl; + } + } + + auto end = std::chrono::high_resolution_clock::now(); + auto totalDuration = std::chrono::duration_cast(end - start); + + std::cout << "Easy puzzles: solved " << solvedCount << "/" << easyPuzzles.size() + << " in " << totalDuration.count() << " seconds" << std::endl; + + EXPECT_EQ(solvedCount, easyPuzzles.size()) << "Not all easy puzzles were solved"; +} + +TEST_F(SudokuTest, LoadAndSolveMediumPuzzles) +{ + std::vector mediumPuzzles = loadPuzzlesFromFile("/home/connor/repos/nd-wfc/demos/sudoku/data/Sudoku_medium.txt"); + + ASSERT_GT(mediumPuzzles.size(), 0) << "No medium puzzles loaded"; + + int solvedCount = 0; + auto start = std::chrono::high_resolution_clock::now(); + + for (size_t i = 0; i < mediumPuzzles.size(); ++i) { + auto& sudoku = mediumPuzzles[i]; + EXPECT_TRUE(sudoku.isValid()) << "Puzzle " << i << " is not valid"; + + auto puzzleStart = std::chrono::high_resolution_clock::now(); + sudokuTestSolver.Run(sudoku, true); + auto puzzleEnd = std::chrono::high_resolution_clock::now(); + + EXPECT_TRUE(sudoku.isSolved()) << "Puzzle " << i << " was not solved"; + + if (sudoku.isSolved()) { + solvedCount++; + } + + auto puzzleDuration = std::chrono::duration_cast(puzzleEnd - puzzleStart); + if (i < 5) { // Only print timing for first 5 puzzles to avoid spam + std::cout << "Medium puzzle " << i << " solved in " << puzzleDuration.count() << "ms" << std::endl; + } + } + + auto end = std::chrono::high_resolution_clock::now(); + auto totalDuration = std::chrono::duration_cast(end - start); + + std::cout << "Medium puzzles: solved " << solvedCount << "/" << mediumPuzzles.size() + << " in " << totalDuration.count() << " seconds" << std::endl; + + EXPECT_EQ(solvedCount, mediumPuzzles.size()) << "Not all medium puzzles were solved"; +} + +TEST_F(SudokuTest, LoadAndSolveHardPuzzles) +{ + std::vector hardPuzzles = loadPuzzlesFromFile("/home/connor/repos/nd-wfc/demos/sudoku/data/Sudoku_hard.txt"); + + ASSERT_GT(hardPuzzles.size(), 0) << "No hard puzzles loaded"; + + int solvedCount = 0; + auto start = std::chrono::high_resolution_clock::now(); + + for (size_t i = 0; i < hardPuzzles.size(); ++i) { + auto& sudoku = hardPuzzles[i]; + EXPECT_TRUE(sudoku.isValid()) << "Puzzle " << i << " is not valid"; + + auto puzzleStart = std::chrono::high_resolution_clock::now(); + sudokuTestSolver.Run(sudoku, true); + auto puzzleEnd = std::chrono::high_resolution_clock::now(); + + EXPECT_TRUE(sudoku.isSolved()) << "Puzzle " << i << " was not solved"; + + if (sudoku.isSolved()) { + solvedCount++; + } + + auto puzzleDuration = std::chrono::duration_cast(puzzleEnd - puzzleStart); + if (i < 5) { // Only print timing for first 5 puzzles to avoid spam + std::cout << "Hard puzzle " << i << " solved in " << puzzleDuration.count() << "ms" << std::endl; + } + } + + auto end = std::chrono::high_resolution_clock::now(); + auto totalDuration = std::chrono::duration_cast(end - start); + + std::cout << "Hard puzzles: solved " << solvedCount << "/" << hardPuzzles.size() + << " in " << totalDuration.count() << " seconds" << std::endl; + + EXPECT_EQ(solvedCount, hardPuzzles.size()) << "Not all hard puzzles were solved"; +} + +TEST_F(SudokuTest, LoadAndSolveDiabolicalPuzzles) +{ + std::vector diabolicalPuzzles = loadPuzzlesFromFile("/home/connor/repos/nd-wfc/demos/sudoku/data/Sudoku_diabolical.txt"); + + ASSERT_GT(diabolicalPuzzles.size(), 0) << "No diabolical puzzles loaded"; + + int solvedCount = 0; + auto start = std::chrono::high_resolution_clock::now(); + + for (size_t i = 0; i < diabolicalPuzzles.size(); ++i) { + auto& sudoku = diabolicalPuzzles[i]; + EXPECT_TRUE(sudoku.isValid()) << "Puzzle " << i << " is not valid"; + + auto puzzleStart = std::chrono::high_resolution_clock::now(); + sudokuTestSolver.Run(sudoku, true); + auto puzzleEnd = std::chrono::high_resolution_clock::now(); + + EXPECT_TRUE(sudoku.isSolved()) << "Puzzle " << i << " was not solved"; + + if (sudoku.isSolved()) { + solvedCount++; + } + + auto puzzleDuration = std::chrono::duration_cast(puzzleEnd - puzzleStart); + if (i < 5) { // Only print timing for first 5 puzzles to avoid spam + std::cout << "Diabolical puzzle " << i << " solved in " << puzzleDuration.count() << "ms" << std::endl; + } + } + + auto end = std::chrono::high_resolution_clock::now(); + auto totalDuration = std::chrono::duration_cast(end - start); + + std::cout << "Diabolical puzzles: solved " << solvedCount << "/" << diabolicalPuzzles.size() + << " in " << totalDuration.count() << " seconds" << std::endl; + + EXPECT_EQ(solvedCount, diabolicalPuzzles.size()) << "Not all diabolical puzzles were solved"; +} + +// Test loading a single puzzle from each difficulty file +TEST_F(SudokuTest, LoadAndSolveFirstPuzzleFromEachFile) +{ + const std::string dataPath = "/home/connor/repos/nd-wfc/demos/sudoku/data"; + const std::vector files = {"Sudoku_easy.txt", "Sudoku_medium.txt", "Sudoku_hard.txt", "Sudoku_diabolical.txt"}; + + for (const auto& filename : files) { + std::string filepath = dataPath + "/" + filename; + std::ifstream file(filepath); + + ASSERT_TRUE(file.is_open()) << "Failed to open file " << filename; + + std::string firstLine; + std::getline(file, firstLine); + + // Remove whitespace + firstLine.erase(std::remove_if(firstLine.begin(), firstLine.end(), + [](char c) { return std::isspace(c); }), firstLine.end()); + + ASSERT_FALSE(firstLine.empty()) << "No puzzle data found in " << filename; + + Sudoku puzzle; + ASSERT_TRUE(puzzle.loadFromString(firstLine)) << "Failed to load puzzle from first line of " << filename; + + EXPECT_TRUE(puzzle.isValid()) << "Loaded puzzle from " << filename << " is not valid"; + + auto start = std::chrono::high_resolution_clock::now(); + sudokuTestSolver.Run(puzzle, true); + auto end = std::chrono::high_resolution_clock::now(); + + EXPECT_TRUE(puzzle.isSolved()) << "Failed to solve first puzzle from " << filename; + + auto duration = std::chrono::duration_cast(end - start); + std::cout << "First puzzle from " << filename << " solved in " << duration.count() << "ms" << std::endl; + } +} + +// Helper function to load multiple puzzles from a file (one puzzle per line) +std::vector loadPuzzlesFromFile(const std::string& filename) { + std::vector puzzles; + std::ifstream file(filename); + + if (!file.is_open()) { + return puzzles; + } + + std::string line; + while (std::getline(file, line)) { + // Remove whitespace + line.erase(std::remove_if(line.begin(), line.end(), + [](char c) { return std::isspace(c); }), line.end()); + + if (line.empty()) continue; + + Sudoku sudoku; + if (sudoku.loadFromString(line)) { + puzzles.push_back(std::move(sudoku)); + } + } + + return puzzles; +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); diff --git a/include/nd-wfc/wfc.hpp b/include/nd-wfc/wfc.hpp index 2b3f3b5..e811349 100644 --- a/include/nd-wfc/wfc.hpp +++ b/include/nd-wfc/wfc.hpp @@ -260,13 +260,15 @@ public: Wave wave; std::mt19937& rng; WFCStackAllocator& allocator; + size_t& iterations; - SolverState(WorldT& world, size_t variableAmount, std::mt19937& rng, WFCStackAllocator& allocator) + SolverState(WorldT& world, size_t variableAmount, std::mt19937& rng, WFCStackAllocator& allocator, size_t& iterations) : world(world) , propagationQueue{ WFCStackAllocatorAdapter(allocator) } , wave{ world.size(), variableAmount, allocator } , rng(rng) , allocator(allocator) + , iterations(iterations) {} SolverState(const SolverState& other) = default; @@ -282,7 +284,8 @@ public: { WFCStackAllocator allocator{}; std::mt19937 random{ std::random_device{}() }; - SolverState state(world, m_variables.size(), random, allocator); + size_t iterations = 0; + SolverState state(world, m_variables.size(), random, allocator, iterations); return Run(state, propagateInitialValues); } @@ -307,8 +310,7 @@ public: bool RunLoop(SolverState& state) { - constexpr size_t maxIterations = 1024; - for (size_t i = 0; i < maxIterations; ++i) + for (; state.iterations < 256; ++state.iterations) { if (!Propagate(state)) return false;