fixed tests

This commit is contained in:
2025-09-01 09:15:35 +09:00
parent 8dc7bcd618
commit e9a298e9ef
19 changed files with 228 additions and 1853 deletions

View File

@@ -1,24 +0,0 @@
# Console Renderer Demo
add_executable(sudoku_demo_renderer
sudoku_demo_renderer.cpp
console_renderer.cpp
sudoku/sudoku.cpp
)
target_include_directories(sudoku_demo_renderer PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_SOURCE_DIR}/..
)
# Link threading library for std::thread (platform-specific)
set(THREADS_PREFER_PTHREAD_FLAG ON)
find_package(Threads REQUIRED)
if(Threads_FOUND)
target_link_libraries(sudoku_demo_renderer Threads::Threads)
endif()
# Set C++17 standard
set_target_properties(sudoku_demo_renderer PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
)

View File

@@ -1,267 +0,0 @@
# Console Rendering System for Puzzle Demos
This directory contains a comprehensive console rendering system designed for real-time puzzle visualization without adding new lines to the console. The system is perfect for showing solving animations and interactive demos.
## Features
- **Real-time Updates**: Render puzzle states without scrolling the console
- **Space Pre-allocation**: Reserve console space before rendering to prevent layout issues
- **Animation Support**: Smooth animations for solving steps and cell highlights
- **Cross-platform**: Works on Linux, macOS, and Windows
- **Multiple Puzzle Types**: Support for Sudoku and Nonogram puzzles
## Quick Start
### Basic Sudoku Rendering
```cpp
#include "console_renderer.h"
#include "sudoku/sudoku.h"
int main() {
// 1. Create a Sudoku puzzle
std::string puzzle = "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
Sudoku sudoku(puzzle);
// 2. Create renderer
SudokuRenderer renderer(sudoku);
// 3. IMPORTANT: Allocate space first!
renderer.allocateSpace();
// 4. Render initial state
renderer.render();
// 5. Update puzzle and re-render (updates in place!)
sudoku.set(0, 0, 5);
renderer.render();
return 0;
}
```
### Animation Example
```cpp
// Animate a cell change with highlighting
renderer.animateCell(row, col, new_value, 200); // 200ms delay
// Show solving progress
renderer.showSolvingProgress("Solving step 1 of 10");
// Highlight specific cells
renderer.renderWithHighlight(highlight_row, highlight_col);
```
## API Reference
### SudokuRenderer
The `SudokuRenderer` class provides a clean API for rendering Sudoku puzzles with real-time updates.
#### Constructor
```cpp
SudokuRenderer(const Sudoku& sudoku)
```
#### Core Methods
```cpp
void allocateSpace() // Reserve console space (call first!)
void render() // Render current puzzle state
void clear() // Clear allocated space
```
#### Animation Methods
```cpp
void renderWithHighlight(int row, int col) // Highlight specific cell
void showSolvingProgress(const std::string& status) // Update status line
void animateCell(int row, int col, uint8_t value, int delay_ms = 100) // Animate cell change
```
### NonogramRenderer
The `NonogramRenderer` class handles Nonogram puzzle visualization with hints and solution grid.
#### Constructor
```cpp
NonogramRenderer(const Nonogram& nonogram)
```
#### Core Methods
```cpp
void allocateSpace() // Reserve console space
void render() // Render puzzle with hints
void clear() // Clear allocated space
```
#### Nonogram-specific Methods
```cpp
void renderWithState(const std::vector<std::vector<int>>& state) // Render with current solving state
void showSolvingProgress(const std::string& status) // Update status
```
### ConsoleRenderer (Base Class)
Provides low-level console control utilities.
#### Static Utilities
```cpp
static void moveCursorUp(int lines)
static void moveCursorDown(int lines)
static void moveCursorToPosition(int row, int col)
static void clearLine()
static void clearScreen()
static void hideCursor()
static void showCursor()
static void sleep(int milliseconds)
static std::string repeatChar(char c, int count)
static std::string centerText(const std::string& text, int width)
```
### DemoAnimator
Helper class for creating smooth demo animations.
```cpp
static void typewriterEffect(const std::string& text, int delay_ms = 50)
static void fadeIn(const std::vector<std::string>& lines, int delay_ms = 100)
static void progressBar(const std::string& label, int current, int total, int width = 30)
```
## Important Usage Notes
### 1. Always Allocate Space First
**Critical**: Call `allocateSpace()` before any rendering operations:
```cpp
SudokuRenderer renderer(sudoku);
renderer.allocateSpace(); // This MUST be called first!
renderer.render();
```
This reserves the necessary console lines and prevents the display from being corrupted by new output.
### 2. Real-time Updates
After calling `allocateSpace()`, all subsequent `render()` calls will update the display in place:
```cpp
renderer.allocateSpace(); // Once at the start
renderer.render(); // Initial display
// Update puzzle state
sudoku.set(0, 0, 5);
renderer.render(); // Updates in place - no new lines!
sudoku.set(1, 1, 3);
renderer.render(); // Updates in place again
```
### 3. Animation Best Practices
For smooth animations, use the provided animation methods:
```cpp
// Good: Use built-in animation
renderer.animateCell(row, col, value, 200);
// Also good: Manual animation with proper timing
for (int i = 0; i < steps.size(); ++i) {
renderer.showSolvingProgress("Step " + std::to_string(i + 1));
// Update state
renderer.render();
ConsoleRenderer::sleep(300);
}
```
### 4. Error Handling
```cpp
try {
ConsoleRenderer::hideCursor(); // For cleaner display
SudokuRenderer renderer(sudoku);
renderer.allocateSpace();
renderer.render();
// ... your demo logic ...
ConsoleRenderer::showCursor(); // Always restore cursor
} catch (const std::exception& e) {
ConsoleRenderer::showCursor(); // Restore cursor on error
std::cerr << "Error: " << e.what() << std::endl;
}
```
## Building
### Using CMake
```bash
cd /testbed/demos
mkdir build && cd build
cmake ..
make sudoku_demo_renderer
./sudoku_demo_renderer
```
### Manual Compilation
```bash
cd /testbed/demos
g++ -std=c++17 -I. -I.. -pthread \
sudoku_demo_renderer.cpp console_renderer.cpp sudoku/sudoku.cpp \
-o sudoku_demo_renderer
./sudoku_demo_renderer
```
## Demo Programs
### sudoku_demo_renderer
A comprehensive demonstration showing:
- Basic Sudoku rendering
- Real-time puzzle updates
- Solving animations
- API usage examples
```bash
./sudoku_demo_renderer
```
### test_renderer
Simple test program to verify the renderer functionality:
```bash
./test_renderer
```
## Technical Details
### Console Control
The system uses ANSI escape sequences for cursor control:
- `\033[nA` - Move cursor up n lines
- `\033[nB` - Move cursor down n lines
- `\033[2K` - Clear current line
- `\033[?25l` - Hide cursor
- `\033[?25h` - Show cursor
### Memory Efficiency
- Sudoku puzzles use only 41 bytes of storage
- Nonogram puzzles use bit-packed storage for solutions
- Renderers don't store puzzle state - they reference existing objects
### Cross-platform Support
The renderer works on:
- Linux (tested)
- macOS (ANSI escape sequences supported)
- Windows (with ANSI support enabled)
## Examples
See the demo programs for complete working examples:
- `sudoku_demo_renderer.cpp` - Full-featured Sudoku demo
- `test_renderer.cpp` - Basic functionality test
The console rendering system provides a powerful foundation for creating engaging puzzle demos and interactive solving visualizations!

View File

@@ -1,509 +0,0 @@
#include "console_renderer.h"
#include "sudoku/sudoku.h"
#include "nonogram/nonogram.h"
#include <iostream>
#include <iomanip>
// ANSI escape codes for cursor control
#ifdef _WIN32
#include <windows.h>
#include <conio.h>
#else
#include <unistd.h>
#include <termios.h>
#endif
// =============================================================================
// ConsoleRenderer Implementation
// =============================================================================
void ConsoleRenderer::moveCursorUp(int lines) {
if (lines > 0) {
std::cout << "\033[" << lines << "A";
std::cout.flush();
}
}
void ConsoleRenderer::moveCursorDown(int lines) {
if (lines > 0) {
std::cout << "\033[" << lines << "B";
std::cout.flush();
}
}
void ConsoleRenderer::moveCursorToColumn(int col) {
std::cout << "\033[" << col << "G";
std::cout.flush();
}
void ConsoleRenderer::moveCursorToPosition(int row, int col) {
std::cout << "\033[" << row << ";" << col << "H";
std::cout.flush();
}
void ConsoleRenderer::clearLine() {
std::cout << "\033[2K";
std::cout.flush();
}
void ConsoleRenderer::clearScreen() {
std::cout << "\033[2J\033[H";
std::cout.flush();
}
void ConsoleRenderer::hideCursor() {
std::cout << "\033[?25l";
std::cout.flush();
}
void ConsoleRenderer::showCursor() {
std::cout << "\033[?25h";
std::cout.flush();
}
void ConsoleRenderer::sleep(int milliseconds) {
std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds));
}
std::string ConsoleRenderer::repeatChar(char c, int count) {
return std::string(count, c);
}
std::string ConsoleRenderer::centerText(const std::string& text, int width) {
if (static_cast<int>(text.length()) >= width) return text;
int padding = (width - static_cast<int>(text.length())) / 2;
return repeatChar(' ', padding) + text;
}
// =============================================================================
// SudokuRenderer Implementation
// =============================================================================
SudokuRenderer::SudokuRenderer(const Sudoku& sudoku) : sudoku_(sudoku) {}
void SudokuRenderer::allocateSpace() {
if (space_allocated_) return;
// Print empty lines to reserve space
for (int i = 0; i < TOTAL_HEIGHT; ++i) {
std::cout << std::endl;
}
allocated_lines_ = TOTAL_HEIGHT;
space_allocated_ = true;
// Move cursor back to start of allocated space
moveCursorUp(TOTAL_HEIGHT);
}
void SudokuRenderer::render() {
if (!space_allocated_) {
allocateSpace();
}
// Save cursor position
std::cout << "\033[s";
// Render title
std::string title = "SUDOKU PUZZLE";
std::cout << centerText(title, GRID_WIDTH) << std::endl;
std::cout << centerText(repeatChar('=', title.length()), GRID_WIDTH) << std::endl;
std::cout << std::endl;
// Render grid
for (int row = 0; row < 9; ++row) {
std::cout << formatSudokuLine(row) << std::endl;
// Add separator lines after rows 2 and 5
if (row == 2 || row == 5) {
std::cout << getSeparatorLine() << std::endl;
}
}
std::cout << std::endl;
// Render status
std::string status = sudoku_.isSolved() ? "SOLVED!" :
sudoku_.isValid() ? "Valid puzzle" : "Invalid puzzle";
std::cout << centerText(status, GRID_WIDTH) << std::endl;
// Restore cursor position
std::cout << "\033[u";
std::cout.flush();
}
void SudokuRenderer::clear() {
if (!space_allocated_) return;
// Move to start of allocated space
std::cout << "\033[s";
// Clear all lines
for (int i = 0; i < allocated_lines_; ++i) {
clearLine();
if (i < allocated_lines_ - 1) {
moveCursorDown(1);
}
}
// Restore cursor position
std::cout << "\033[u";
std::cout.flush();
}
void SudokuRenderer::renderWithHighlight(int highlight_row, int highlight_col) {
if (!space_allocated_) {
allocateSpace();
}
// Save cursor position
std::cout << "\033[s";
// Render title
std::string title = "SUDOKU PUZZLE";
std::cout << centerText(title, GRID_WIDTH) << std::endl;
std::cout << centerText(repeatChar('=', title.length()), GRID_WIDTH) << std::endl;
std::cout << std::endl;
// Render grid with highlighting
for (int row = 0; row < 9; ++row) {
std::string line = formatSudokuLine(row);
// Apply highlighting if this is the target row
if (row == highlight_row && highlight_col >= 0 && highlight_col < 9) {
// Calculate position of highlighted cell in the line
int pos = highlight_col * 2; // Each cell takes 2 chars ("X ")
if (highlight_col >= 3) pos += 2; // Account for " | " separator
if (highlight_col >= 6) pos += 2; // Account for second " | " separator
// Insert highlighting safely
std::string highlighted = line;
if (pos >= 0 && pos < static_cast<int>(highlighted.length())) {
// Insert reverse video before the character
highlighted.insert(pos, "\033[7m");
// Insert reset after the character (accounting for the inserted escape sequence)
if (pos + 5 < static_cast<int>(highlighted.length())) {
highlighted.insert(pos + 5, "\033[0m");
}
}
line = highlighted;
}
std::cout << line << std::endl;
// Add separator lines after rows 2 and 5
if (row == 2 || row == 5) {
std::cout << getSeparatorLine() << std::endl;
}
}
std::cout << std::endl;
// Render status
std::string status = sudoku_.isSolved() ? "SOLVED!" :
sudoku_.isValid() ? "Valid puzzle" : "Invalid puzzle";
std::cout << centerText(status, GRID_WIDTH) << std::endl;
// Restore cursor position
std::cout << "\033[u";
std::cout.flush();
}
void SudokuRenderer::showSolvingProgress(const std::string& status) {
if (!space_allocated_) return;
// Move to status line (last line of allocated space)
std::cout << "\033[s";
moveCursorDown(allocated_lines_ - 1);
clearLine();
std::string display_status = status.empty() ? "Solving..." : status;
std::cout << centerText(display_status, GRID_WIDTH);
std::cout << "\033[u";
std::cout.flush();
}
void SudokuRenderer::animateCell(int row, int col, uint8_t value, int delay_ms) {
// Highlight the cell
renderWithHighlight(row, col);
sleep(delay_ms);
// Render normally
render();
}
std::string SudokuRenderer::formatSudokuLine(int row) const {
std::ostringstream oss;
for (int col = 0; col < 9; ++col) {
uint8_t value = sudoku_.get(row, col);
oss << getCellChar(value);
if (col < 8) {
oss << " ";
// Add vertical separator after columns 2 and 5
if (col == 2 || col == 5) {
oss << "| ";
}
}
}
return oss.str();
}
std::string SudokuRenderer::getSeparatorLine() const {
return "------+-------+------";
}
char SudokuRenderer::getCellChar(uint8_t value) const {
return (value == 0) ? '.' : static_cast<char>('0' + value);
}
// =============================================================================
// NonogramRenderer Implementation
// =============================================================================
NonogramRenderer::NonogramRenderer(const Nonogram& nonogram) : nonogram_(nonogram) {}
void NonogramRenderer::allocateSpace() {
if (space_allocated_) return;
int height = calculateHeight();
// Print empty lines to reserve space
for (int i = 0; i < height; ++i) {
std::cout << std::endl;
}
allocated_lines_ = height;
space_allocated_ = true;
// Move cursor back to start of allocated space
moveCursorUp(height);
}
void NonogramRenderer::render() {
if (!space_allocated_) {
allocateSpace();
}
// Save cursor position
std::cout << "\033[s";
// Render title
std::string title = "NONOGRAM PUZZLE";
int width = calculateWidth();
std::cout << centerText(title, width) << std::endl;
std::cout << centerText(repeatChar('=', title.length()), width) << std::endl;
std::cout << std::endl;
// Render column hints
std::cout << formatColumnHints() << std::endl;
std::cout << std::endl;
// Render grid with row hints
for (size_t row = 0; row < nonogram_.getHeight(); ++row) {
std::cout << formatNonogramLine(static_cast<int>(row)) << std::endl;
}
std::cout << std::endl;
// Render status
std::string status = "Nonogram loaded";
std::cout << centerText(status, width) << std::endl;
// Restore cursor position
std::cout << "\033[u";
std::cout.flush();
}
void NonogramRenderer::clear() {
if (!space_allocated_) return;
// Move to start of allocated space
std::cout << "\033[s";
// Clear all lines
for (int i = 0; i < allocated_lines_; ++i) {
clearLine();
if (i < allocated_lines_ - 1) {
moveCursorDown(1);
}
}
// Restore cursor position
std::cout << "\033[u";
std::cout.flush();
}
void NonogramRenderer::renderWithState(const std::vector<std::vector<int>>& state) {
if (!space_allocated_) {
allocateSpace();
}
// Save cursor position
std::cout << "\033[s";
// Render title
std::string title = "NONOGRAM PUZZLE";
int width = calculateWidth();
std::cout << centerText(title, width) << std::endl;
std::cout << centerText(repeatChar('=', title.length()), width) << std::endl;
std::cout << std::endl;
// Render column hints
std::cout << formatColumnHints() << std::endl;
std::cout << std::endl;
// Render grid with current state
for (size_t row = 0; row < nonogram_.getHeight(); ++row) {
std::cout << formatNonogramLine(static_cast<int>(row), &state) << std::endl;
}
std::cout << std::endl;
// Render status
std::string status = "Solving...";
std::cout << centerText(status, width) << std::endl;
// Restore cursor position
std::cout << "\033[u";
std::cout.flush();
}
void NonogramRenderer::showSolvingProgress(const std::string& status) {
if (!space_allocated_) return;
// Move to status line
std::cout << "\033[s";
moveCursorDown(allocated_lines_ - 1);
clearLine();
std::string display_status = status.empty() ? "Solving..." : status;
std::cout << centerText(display_status, calculateWidth());
std::cout << "\033[u";
std::cout.flush();
}
std::string NonogramRenderer::formatNonogramLine(int row, const std::vector<std::vector<int>>* state) const {
std::ostringstream oss;
// Add row hints (right-aligned in a fixed width)
auto hints = nonogram_.getRowHints(row);
std::ostringstream hints_oss;
for (size_t i = 0; i < hints.size(); ++i) {
if (i > 0) hints_oss << " ";
hints_oss << static_cast<int>(hints[i]);
}
std::string hints_str = hints_oss.str();
// Right-align hints in 8 character field
oss << std::setw(8) << hints_str << " | ";
// Add grid cells
for (size_t col = 0; col < nonogram_.getWidth(); ++col) {
char cell_char = '?';
if (state && row < static_cast<int>(state->size()) && col < state->at(row).size()) {
int cell_state = state->at(row)[col];
if (cell_state == 1) cell_char = '#';
else if (cell_state == 0) cell_char = '.';
} else if (nonogram_.hasSolution()) {
cell_char = nonogram_.getSolutionCell(row, col) ? '#' : '.';
}
oss << cell_char;
if (col < nonogram_.getWidth() - 1) oss << " ";
}
return oss.str();
}
std::string NonogramRenderer::formatColumnHints() const {
std::ostringstream oss;
// Find max hint height for columns
int max_hint_height = 0;
for (size_t col = 0; col < nonogram_.getColumnCount(); ++col) {
auto hints = nonogram_.getColumnHints(col);
max_hint_height = std::max(max_hint_height, static_cast<int>(hints.size()));
}
// Render column hints from top to bottom
for (int hint_row = 0; hint_row < max_hint_height; ++hint_row) {
oss << std::setw(8) << " " << " | "; // Space for row hints
for (size_t col = 0; col < nonogram_.getColumnCount(); ++col) {
auto hints = nonogram_.getColumnHints(col);
// Calculate which hint to show (from top)
int hint_index = hint_row - (max_hint_height - static_cast<int>(hints.size()));
if (hint_index >= 0 && hint_index < static_cast<int>(hints.size())) {
oss << static_cast<int>(hints[hint_index]);
} else {
oss << " ";
}
if (col < nonogram_.getColumnCount() - 1) oss << " ";
}
if (hint_row < max_hint_height - 1) oss << std::endl;
}
return oss.str();
}
int NonogramRenderer::calculateWidth() const {
return 10 + static_cast<int>(nonogram_.getWidth()) * 2; // hints + grid
}
int NonogramRenderer::calculateHeight() const {
// Title (2) + column hints (variable) + grid + status (3)
int max_col_hints = 0;
for (size_t col = 0; col < nonogram_.getColumnCount(); ++col) {
auto hints = nonogram_.getColumnHints(col);
max_col_hints = std::max(max_col_hints, static_cast<int>(hints.size()));
}
return 2 + max_col_hints + 1 + static_cast<int>(nonogram_.getHeight()) + 3;
}
// =============================================================================
// DemoAnimator Implementation
// =============================================================================
void DemoAnimator::typewriterEffect(const std::string& text, int delay_ms) {
for (char c : text) {
std::cout << c;
std::cout.flush();
ConsoleRenderer::sleep(delay_ms);
}
}
void DemoAnimator::fadeIn(const std::vector<std::string>& lines, int delay_ms) {
for (const auto& line : lines) {
std::cout << line << std::endl;
ConsoleRenderer::sleep(delay_ms);
}
}
void DemoAnimator::progressBar(const std::string& label, int current, int total, int width) {
double percentage = static_cast<double>(current) / total;
int filled = static_cast<int>(percentage * width);
std::cout << "\r" << label << " [";
for (int i = 0; i < width; ++i) {
if (i < filled) {
std::cout << "=";
} else if (i == filled) {
std::cout << ">";
} else {
std::cout << " ";
}
}
std::cout << "] " << static_cast<int>(percentage * 100) << "%";
std::cout.flush();
}

View File

@@ -1,130 +0,0 @@
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <chrono>
#include <thread>
/**
* Base Console Renderer
*
* Provides utilities for console-based rendering with cursor control,
* allowing for real-time updates without adding new lines.
*
* Key features:
* - Allocate screen space before rendering
* - Move cursor to update specific areas
* - Clear and redraw content in place
* - Cross-platform cursor control
*/
class ConsoleRenderer {
public:
ConsoleRenderer() = default;
virtual ~ConsoleRenderer() = default;
// Core rendering interface
virtual void allocateSpace() = 0;
virtual void render() = 0;
virtual void clear() = 0;
// Cursor control utilities
static void moveCursorUp(int lines);
static void moveCursorDown(int lines);
static void moveCursorToColumn(int col);
static void moveCursorToPosition(int row, int col);
static void clearLine();
static void clearScreen();
static void hideCursor();
static void showCursor();
// Helper functions
static void sleep(int milliseconds);
static std::string repeatChar(char c, int count);
static std::string centerText(const std::string& text, int width);
protected:
int allocated_lines_ = 0;
bool space_allocated_ = false;
};
/**
* Sudoku Console Renderer
*
* Renders Sudoku puzzles with proper grid formatting and supports
* real-time updates for solving animations.
*
* Usage:
* SudokuRenderer renderer(sudoku);
* renderer.allocateSpace(); // Reserve console space
* renderer.render(); // Initial render
*
* // Update sudoku state and re-render
* sudoku.set(0, 0, 5);
* renderer.render(); // Updates in place
*/
class SudokuRenderer : public ConsoleRenderer {
public:
explicit SudokuRenderer(const class Sudoku& sudoku);
void allocateSpace() override;
void render() override;
void clear() override;
// Sudoku-specific methods
void renderWithHighlight(int highlight_row = -1, int highlight_col = -1);
void showSolvingProgress(const std::string& status = "");
// Animation support
void animateCell(int row, int col, uint8_t value, int delay_ms = 100);
private:
const class Sudoku& sudoku_;
// Rendering helpers
std::string formatSudokuLine(int row) const;
std::string getSeparatorLine() const;
char getCellChar(uint8_t value) const;
// Constants for formatting
static constexpr int GRID_WIDTH = 21; // "1 2 3 | 4 5 6 | 7 8 9"
static constexpr int GRID_HEIGHT = 11; // 9 rows + 2 separators
static constexpr int TOTAL_HEIGHT = 15; // Grid + title + status
};
/**
* Nonogram Console Renderer
*
* Renders Nonogram puzzles with hints and solution grid.
* Supports real-time updates for solving animations.
*/
class NonogramRenderer : public ConsoleRenderer {
public:
explicit NonogramRenderer(const class Nonogram& nonogram);
void allocateSpace() override;
void render() override;
void clear() override;
// Nonogram-specific methods
void renderWithState(const std::vector<std::vector<int>>& state);
void showSolvingProgress(const std::string& status = "");
private:
const class Nonogram& nonogram_;
// Rendering helpers
std::string formatNonogramLine(int row, const std::vector<std::vector<int>>* state = nullptr) const;
std::string formatColumnHints() const;
int calculateWidth() const;
int calculateHeight() const;
};
// Utility class for demo animations
class DemoAnimator {
public:
static void typewriterEffect(const std::string& text, int delay_ms = 50);
static void fadeIn(const std::vector<std::string>& lines, int delay_ms = 100);
static void progressBar(const std::string& label, int current, int total, int width = 30);
};

View File

@@ -48,11 +48,7 @@ if(MSVC)
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
endif() endif()
# Create the main executable
add_executable(sudoku_demo
main.cpp
sudoku.cpp
)
# Create WFC demo executable # Create WFC demo executable
add_executable(sudoku_wfc_demo add_executable(sudoku_wfc_demo
@@ -60,36 +56,19 @@ add_executable(sudoku_wfc_demo
sudoku.cpp 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
)
# Link all executables to the nd-wfc library
target_link_libraries(sudoku_demo PRIVATE nd-wfc)
# Link executable to the nd-wfc library
target_link_libraries(sudoku_wfc_demo PRIVATE nd-wfc) target_link_libraries(sudoku_wfc_demo PRIVATE nd-wfc)
target_link_libraries(analyze_failing_puzzles PRIVATE nd-wfc)
target_link_libraries(debug_failing_puzzles PRIVATE nd-wfc)
# Ensure consistent runtime library settings for all executables # Ensure consistent runtime library settings for executable
if(MSVC) if(MSVC)
target_compile_options(sudoku_demo PRIVATE $<$<CONFIG:Debug>:/MDd> $<$<CONFIG:Release>:/MD>)
target_compile_options(sudoku_wfc_demo PRIVATE $<$<CONFIG:Debug>:/MDd> $<$<CONFIG:Release>:/MD>) target_compile_options(sudoku_wfc_demo PRIVATE $<$<CONFIG:Debug>:/MDd> $<$<CONFIG:Release>:/MD>)
target_compile_options(analyze_failing_puzzles PRIVATE $<$<CONFIG:Debug>:/MDd> $<$<CONFIG:Release>:/MD>)
target_compile_options(debug_failing_puzzles PRIVATE $<$<CONFIG:Debug>:/MDd> $<$<CONFIG:Release>:/MD>)
endif() endif()
# Set output directory for sudoku_demo
set_target_properties(sudoku_demo PROPERTIES
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin
)
# Set output directory and properties for sudoku_wfc_demo # Set output directory and properties for sudoku_wfc_demo
set_target_properties(sudoku_wfc_demo PROPERTIES set_target_properties(sudoku_wfc_demo PROPERTIES
@@ -98,38 +77,19 @@ set_target_properties(sudoku_wfc_demo PROPERTIES
CXX_STANDARD_REQUIRED ON 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 (current source directory for local headers) # Include directories (current source directory for local headers)
target_include_directories(sudoku_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_include_directories(sudoku_wfc_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) target_include_directories(sudoku_wfc_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_include_directories(analyze_failing_puzzles PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_include_directories(debug_failing_puzzles PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# Optional: Enable optimizations for release builds # Optional: Enable optimizations for release builds
if(CMAKE_BUILD_TYPE STREQUAL "Release") if(CMAKE_BUILD_TYPE STREQUAL "Release")
if(MSVC) if(MSVC)
target_compile_options(sudoku_demo PRIVATE /O2)
target_compile_options(sudoku_wfc_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() else()
target_compile_options(sudoku_demo PRIVATE -O3 -march=native)
target_compile_options(sudoku_wfc_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()
endif() endif()
@@ -193,9 +153,9 @@ endif()
# Installation (optional) # Installation (optional)
if(HAS_GTEST AND HAS_BENCHMARK) if(HAS_GTEST AND HAS_BENCHMARK)
install(TARGETS sudoku_demo sudoku_wfc_demo analyze_failing_puzzles debug_failing_puzzles sudoku_tests sudoku_benchmarks DESTINATION bin) install(TARGETS sudoku_wfc_demo sudoku_tests sudoku_benchmarks DESTINATION bin)
elseif(HAS_GTEST) elseif(HAS_GTEST)
install(TARGETS sudoku_demo sudoku_wfc_demo analyze_failing_puzzles debug_failing_puzzles sudoku_tests DESTINATION bin) install(TARGETS sudoku_wfc_demo sudoku_tests DESTINATION bin)
else() else()
install(TARGETS sudoku_demo sudoku_wfc_demo analyze_failing_puzzles debug_failing_puzzles DESTINATION bin) install(TARGETS sudoku_wfc_demo DESTINATION bin)
endif() endif()

View File

@@ -1,158 +0,0 @@
#include "sudoku.h"
#include <nd-wfc/wfc.hpp>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <chrono>
#include <algorithm>
// Helper function to load multiple puzzles from a file (one puzzle per line)
std::vector<Sudoku> loadPuzzlesFromFile(const std::string& filename) {
std::vector<Sudoku> 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<std::string>& 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() {
// File paths
const std::string dataPath = "/home/connor/repos/nd-wfc/demos/sudoku/data";
const std::vector<std::string> 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<std::string> allFailingPuzzles;
std::vector<std::pair<std::string, int>> 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<std::string> 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<std::chrono::milliseconds>(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<std::chrono::seconds>(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;
}

View File

@@ -1,165 +0,0 @@
#include "sudoku.h"
#include <nd-wfc/wfc.hpp>
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <chrono>
#include <algorithm>
#include <iomanip>
// Helper function to load puzzles from a file (one puzzle per line)
std::vector<Sudoku> loadPuzzlesFromFile(const std::string& filename) {
std::vector<Sudoku> 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<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::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
// 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<std::chrono::milliseconds>(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<std::chrono::milliseconds>(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;
}

View File

@@ -1,19 +0,0 @@
#include "sudoku.h"
#include <iostream>
int main()
{
std::cout << "Sudoku Demo" << std::endl;
// Create a simple sudoku puzzle
Sudoku sudoku("140000050700200000000300204200080400080090020006050001809001000000006007050000069");
if (sudoku.isValid()) {
std::cout << "Loaded valid sudoku puzzle:" << std::endl;
sudoku.print();
} else {
std::cout << "Invalid sudoku puzzle!" << std::endl;
}
return 0;
}

View File

@@ -6,39 +6,6 @@
#include <cctype> #include <cctype>
#include <bitset> #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) { bool Sudoku::loadFromFile(const std::string& filename) {
std::ifstream file(filename); std::ifstream file(filename);
if (!file.is_open()) { if (!file.is_open()) {
@@ -60,68 +27,11 @@ bool Sudoku::loadFromFile(const std::string& filename) {
return loadFromString(puzzle_str); return loadFromString(puzzle_str);
} }
void Sudoku::print(char emptyVal) const {
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 row = 0; row < 9; ++row) {
for (int col = 0; col < 9; ++col) { for (int col = 0; col < 9; ++col) {
uint8_t value = get(row, col); uint8_t value = get(row, col);
std::cout << (value == 0 ? '.' : static_cast<char>('0' + value)); std::cout << (value == 0 ? emptyVal : static_cast<char>('0' + value));
if (col < 8) std::cout << " "; if (col < 8) std::cout << " ";
if (col == 2 || col == 5) std::cout << "| "; if (col == 2 || col == 5) std::cout << "| ";
} }
@@ -132,12 +42,12 @@ void Sudoku::print() const {
} }
} }
std::string Sudoku::toString() const { std::string Sudoku::toString(char emptyVal) const {
std::string result; std::string result;
result.reserve(81); result.reserve(81);
for (int i = 0; i < 81; ++i) { for (int i = 0; i < 81; ++i) {
uint8_t cell = board_.get(i); uint8_t cell = board_.get(i);
result += (cell == 0 ? '.' : static_cast<char>('0' + cell)); result += (cell == 0 ? emptyVal : static_cast<char>('0' + cell));
} }
return result; return result;
} }

View File

@@ -5,8 +5,12 @@
#include <optional> #include <optional>
#include <cstdint> #include <cstdint>
#include <array> #include <array>
#include <cassert>
#include <chrono> #include <chrono>
#include <bitset>
#include <algorithm>
#include <cctype>
#include <string_view>
#include <nd-wfc/wfc.hpp> #include <nd-wfc/wfc.hpp>
// 4-bit packed Sudoku board storage - optimal packing // 4-bit packed Sudoku board storage - optimal packing
@@ -15,14 +19,14 @@
// 81 cells / 2 = 40.5 bytes → 41 bytes total (with 4 bits unused) // 81 cells / 2 = 40.5 bytes → 41 bytes total (with 4 bits unused)
class SudokuBoardStorage { class SudokuBoardStorage {
public: public:
std::array<uint8_t, 41> data; std::array<uint8_t, 41> data {};
// Get 4-bit value at position (0-80) // Get 4-bit value at position (0-80)
// Each byte contains 2 cells: [cell0(4bits)][cell1(4bits)] // Each byte contains 2 cells: [cell0(4bits)][cell1(4bits)]
// Ultra-fast: only bitwise operations, no divide/modulo! // Ultra-fast: only bitwise operations, no divide/modulo!
// Optimization: pos >> 1 instead of pos / 2 // Optimization: pos >> 1 instead of pos / 2
// Optimization: (pos & 1) << 2 instead of (pos % 2) * 4 // Optimization: (pos & 1) << 2 instead of (pos % 2) * 4
uint8_t get(int pos) const { constexpr inline uint8_t get(int pos) const {
int byteIndex = pos >> 1; // pos / 2 using right shift int byteIndex = pos >> 1; // pos / 2 using right shift
// Precomputed shift amounts: 4 for even positions, 0 for odd positions // Precomputed shift amounts: 4 for even positions, 0 for odd positions
@@ -34,7 +38,7 @@ public:
uint8_t result = (data[byteIndex] >> shiftAmount) & 0xF; uint8_t result = (data[byteIndex] >> shiftAmount) & 0xF;
// Debug assertion: ensure result is in valid range // Debug assertion: ensure result is in valid range
assert(result >= 0 && result <= 9 && "Sudoku cell value must be between 0-9"); WFC::constexpr_assert(result >= 0 && result <= 9, "Sudoku cell value must be between 0-9");
return result; return result;
} }
@@ -43,9 +47,9 @@ public:
// Ultra-fast: only bitwise operations, no divide/modulo! // Ultra-fast: only bitwise operations, no divide/modulo!
// Optimization: pos >> 1 instead of pos / 2 // Optimization: pos >> 1 instead of pos / 2
// Optimization: (pos & 1) << 2 instead of (pos % 2) * 4 // Optimization: (pos & 1) << 2 instead of (pos % 2) * 4
void set(int pos, uint8_t value) { constexpr inline void set(int pos, uint8_t value) {
// Assert that value is in valid Sudoku range (0-9) // Assert that value is in valid Sudoku range (0-9)
assert(value >= 0 && value <= 9 && "Sudoku cell value must be between 0-9"); WFC::constexpr_assert(value >= 0 && value <= 9, "Sudoku cell value must be between 0-9");
int byteIndex = pos >> 1; // pos / 2 using right shift int byteIndex = pos >> 1; // pos / 2 using right shift
@@ -59,7 +63,7 @@ public:
data[byteIndex] = (data[byteIndex] & mask) | (value << shiftAmount); data[byteIndex] = (data[byteIndex] & mask) | (value << shiftAmount);
} }
void clear() { constexpr void clear() {
data.fill(0); data.fill(0);
} }
}; };
@@ -67,23 +71,49 @@ public:
// Ultra-memory-efficient Sudoku class: exactly 41 bytes // Ultra-memory-efficient Sudoku class: exactly 41 bytes
class Sudoku { class Sudoku {
public: public:
Sudoku(); constexpr Sudoku() = default;
explicit Sudoku(const std::string& puzzle_str); constexpr explicit Sudoku(std::string_view puzzle_str)
{
loadFromString(puzzle_str);
}
// Load from various formats // Load from various formats
bool loadFromString(const std::string& puzzle_str); constexpr bool loadFromString(std::string_view 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 loadFromFile(const std::string& filename); bool loadFromFile(const std::string& filename);
// Board access (inlined for performance) // Board access (inlined for performance)
inline uint8_t get(int row, int col) const { constexpr inline uint8_t get(int row, int col) const {
assert((row >= 0 && row < 9 && col >= 0 && col < 9) && WFC::constexpr_assert((row >= 0 && row < 9 && col >= 0 && col < 9),
"Sudoku::get() called with invalid position - row and col must be 0-8"); "Sudoku::get() called with invalid position - row and col must be 0-8");
int linearIndex = getLinearIndex(row, col); int linearIndex = getLinearIndex(row, col);
return board_.get(linearIndex); return board_.get(linearIndex);
} }
inline bool set(int row, int col, uint8_t value) { constexpr inline bool set(int row, int col, uint8_t value) {
assert((row >= 0 && row < 9 && col >= 0 && col < 9) && WFC::constexpr_assert((row >= 0 && row < 9 && col >= 0 && col < 9),
"Sudoku::set() called with invalid position - row and col must be 0-8"); "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 // Keep value validation as runtime check since it's about valid Sudoku numbers
@@ -104,14 +134,64 @@ public:
return true; return true;
} }
void clear(); constexpr void clear() {
board_.clear();
}
// Validation // Validation
bool isValid() const; constexpr bool isValid() const {
bool isSolved() 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;
}
constexpr bool isSolved() const {
for (int i = 0; i < 81; ++i) {
if (board_.get(i) == 0) return false;
}
return isValid();
}
// Inlined validation (called frequently from set()) // Inlined validation (called frequently from set())
inline bool isValidMove(int row, int col, uint8_t value) const { constexpr inline bool isValidMove(int row, int col, uint8_t value) const {
if (value == 0 || value > 9) return false; if (value == 0 || value > 9) return false;
return !hasRowConflictExcluding(row, col, value) && return !hasRowConflictExcluding(row, col, value) &&
!hasColConflictExcluding(col, row, value) && !hasColConflictExcluding(col, row, value) &&
@@ -119,8 +199,8 @@ public:
} }
// Utility // Utility
void print() const; void print(char emptyVal = '.') const;
std::string toString() const; std::string toString(char emptyVal = '.') const;
// Convert to standard board format for external use // Convert to standard board format for external use
std::array<uint8_t, 81> getBoard() const; std::array<uint8_t, 81> getBoard() const;
@@ -129,35 +209,35 @@ private:
SudokuBoardStorage board_; SudokuBoardStorage board_;
// Helper functions (inlined for performance) // Helper functions (inlined for performance)
inline int getLinearIndex(int row, int col) const { constexpr inline int getLinearIndex(int row, int col) const {
return row * 9 + col; return row * 9 + col;
} }
inline int getBoxIndex(int row, int col) const { constexpr inline int getBoxIndex(int row, int col) const {
return (row / 3) * 3 + (col / 3); return (row / 3) * 3 + (col / 3);
} }
inline bool isValidPosition(int row, int col) const { constexpr inline bool isValidPosition(int row, int col) const {
return row >= 0 && row < 9 && col >= 0 && col < 9; return row >= 0 && row < 9 && col >= 0 && col < 9;
} }
// Validation helpers (inlined for performance) // Validation helpers (inlined for performance)
// Uses std::bitset<10> for efficient duplicate detection instead of arrays // Uses std::bitset<10> for efficient duplicate detection instead of arrays
inline bool hasRowConflict(int row, uint8_t value) const { constexpr inline bool hasRowConflict(int row, uint8_t value) const {
for (int col = 0; col < 9; ++col) { for (int col = 0; col < 9; ++col) {
if (get(row, col) == value) return true; if (get(row, col) == value) return true;
} }
return false; return false;
} }
inline bool hasColConflict(int col, uint8_t value) const { constexpr inline bool hasColConflict(int col, uint8_t value) const {
for (int row = 0; row < 9; ++row) { for (int row = 0; row < 9; ++row) {
if (get(row, col) == value) return true; if (get(row, col) == value) return true;
} }
return false; return false;
} }
inline bool hasBoxConflict(int box, uint8_t value) const { constexpr inline bool hasBoxConflict(int box, uint8_t value) const {
int startRow = (box / 3) * 3; int startRow = (box / 3) * 3;
int startCol = (box % 3) * 3; int startCol = (box % 3) * 3;
for (int row = 0; row < 3; ++row) { for (int row = 0; row < 3; ++row) {
@@ -169,21 +249,21 @@ private:
} }
// Validation helpers that exclude current position (for move validation) // Validation helpers that exclude current position (for move validation)
inline bool hasRowConflictExcluding(int row, int excludeCol, uint8_t value) const { constexpr inline bool hasRowConflictExcluding(int row, int excludeCol, uint8_t value) const {
for (int col = 0; col < 9; ++col) { for (int col = 0; col < 9; ++col) {
if (col != excludeCol && get(row, col) == value) return true; if (col != excludeCol && get(row, col) == value) return true;
} }
return false; return false;
} }
inline bool hasColConflictExcluding(int col, int excludeRow, uint8_t value) const { constexpr inline bool hasColConflictExcluding(int col, int excludeRow, uint8_t value) const {
for (int row = 0; row < 9; ++row) { for (int row = 0; row < 9; ++row) {
if (row != excludeRow && get(row, col) == value) return true; if (row != excludeRow && get(row, col) == value) return true;
} }
return false; return false;
} }
inline bool hasBoxConflictExcluding(int box, int excludeRow, int excludeCol, uint8_t value) const { constexpr inline bool hasBoxConflictExcluding(int box, int excludeRow, int excludeCol, uint8_t value) const {
int startRow = (box / 3) * 3; int startRow = (box / 3) * 3;
int startCol = (box % 3) * 3; int startCol = (box % 3) * 3;
for (int row = 0; row < 3; ++row) { for (int row = 0; row < 3; ++row) {
@@ -199,15 +279,15 @@ private:
public: // WFC Support public: // WFC Support
using ValueType = uint8_t; using ValueType = uint8_t;
ValueType getValue(size_t index) const { constexpr inline ValueType getValue(size_t index) const {
return board_.get(static_cast<int>(index)); return board_.get(static_cast<int>(index));
} }
void setValue(size_t index, ValueType value) { constexpr inline void setValue(size_t index, ValueType value) {
board_.set(static_cast<int>(index), value); board_.set(static_cast<int>(index), value);
} }
constexpr size_t size() const { constexpr inline size_t size() const {
return 81; return 81;
} }
}; };
@@ -234,8 +314,6 @@ public:
private: private:
static bool parseLine(const std::string& line, std::array<uint8_t, 81>& board); static bool parseLine(const std::string& line, std::array<uint8_t, 81>& board);
}; };
using SudokuSolverBuilder = WFC::Builder<Sudoku> using SudokuSolverBuilder = WFC::Builder<Sudoku>
::DefineIDs<1, 2, 3, 4, 5, 6, 7, 8, 9> ::DefineIDs<1, 2, 3, 4, 5, 6, 7, 8, 9>
::DefineConstrainer<decltype([](Sudoku&, size_t index, WFC::WorldValue<uint8_t> val, auto& constrainer) { ::DefineConstrainer<decltype([](Sudoku&, size_t index, WFC::WorldValue<uint8_t> val, auto& constrainer) {
@@ -268,4 +346,5 @@ using SudokuSolverBuilder = WFC::Builder<Sudoku>
}), 1, 2, 3, 4, 5, 6, 7, 8, 9>; }), 1, 2, 3, 4, 5, 6, 7, 8, 9>;
using SudokuSolver = SudokuSolverBuilder::Build; using SudokuSolver = SudokuSolverBuilder::Build;

View File

@@ -58,11 +58,16 @@ using SudokuSolverCallback = SudokuSolverBuilder::SetCellCollapsedCallback<declt
})> })>
::Build; ::Build;
Sudoku GetWorldConsteval()
{
return Sudoku{ "6......3.......7....7463....7.8...2.4...9...1.9...7.8....9851....6.......1......9" };
}
int main() int main()
{ {
std::cout << "Running Sudoku WFC" << std::endl; std::cout << "Running Sudoku WFC" << std::endl;
Sudoku sudokuWorld{ "040280030010006007609070008000092000900000004000740000500020803400800010070035090" }; Sudoku sudokuWorld = GetWorldConsteval();
bool success = SudokuSolverCallback::Run(sudokuWorld, true); bool success = SudokuSolverCallback::Run(sudokuWorld, true);
@@ -84,5 +89,5 @@ int main()
if (y == 2 || y == 5) std::cout << "------+-------+------" << std::endl; if (y == 2 || y == 5) std::cout << "------+-------+------" << std::endl;
} }
return (success && solved) ? 0 : 1;
} }

Binary file not shown.

Binary file not shown.

View File

@@ -4,22 +4,17 @@
#include <nd-wfc/wfc.hpp> #include <nd-wfc/wfc.hpp>
#include <fstream> #include <fstream>
#include <algorithm> #include <algorithm>
#include <vector>
#include <string>
#include <iostream>
// Forward declaration for helper function // Forward declarations for helper functions
std::vector<Sudoku> loadPuzzlesFromFile(const std::string& filename); std::vector<Sudoku> loadPuzzlesFromFile(const std::string& filename);
void testPuzzleSolving(const std::string& difficulty, const std::string& filename);
// Test fixture for Sudoku tests // Test fixture for Sudoku tests
class SudokuTest : public ::testing::Test { class SudokuTest : public ::testing::Test {
protected: protected:
void SetUp() override {
// Common test setup
}
void TearDown() override {
// Common test cleanup
}
// Helper function to create a solved Sudoku // Helper function to create a solved Sudoku
Sudoku createSolvedSudoku() { Sudoku createSolvedSudoku() {
Sudoku sudoku; Sudoku sudoku;
@@ -36,9 +31,9 @@ protected:
return sudoku; return sudoku;
} }
Sudoku SolvePuzzle(Sudoku& sudoku) { // Helper function to solve a puzzle using WFC
void solvePuzzle(Sudoku& sudoku) {
SudokuSolver::Run(sudoku, true); SudokuSolver::Run(sudoku, true);
return sudoku;
} }
}; };
@@ -246,7 +241,7 @@ TEST_F(SudokuTest, PerformanceSetOperations) {
int row = i % 9; int row = i % 9;
int col = (i / 9) % 9; int col = (i / 9) % 9;
int value = (i % 9) + 1; int value = (i % 9) + 1;
sudoku.set(row, col, value); sudoku.set(row, col, static_cast<uint8_t>(value));
} }
auto end = std::chrono::high_resolution_clock::now(); auto end = std::chrono::high_resolution_clock::now();
@@ -272,173 +267,82 @@ TEST_F(SudokuTest, EdgeCases) {
EXPECT_EQ(sudoku.get(8, 8), 4); EXPECT_EQ(sudoku.get(8, 8), 4);
} }
TEST_F(SudokuTest, WFCIntegration) TEST_F(SudokuTest, WFCIntegration) {
{ Sudoku sudoku = createEasyPuzzle();
auto sudoku = createEasyPuzzle(); solvePuzzle(sudoku);
SudokuSolver::Run(sudoku, true);
EXPECT_TRUE(sudoku.isSolved()); EXPECT_TRUE(sudoku.isSolved());
} }
// Tests loading and solving puzzles from data files // Unified function to test puzzle solving for different difficulty levels
TEST_F(SudokuTest, LoadAndSolveEasyPuzzles) void testPuzzleSolving(const std::string& difficulty, const std::string& filename) {
{ std::vector<Sudoku> puzzles = loadPuzzlesFromFile(filename);
std::vector<Sudoku> easyPuzzles = loadPuzzlesFromFile("../data/Sudoku_easy.txt");
ASSERT_GT(easyPuzzles.size(), 0) << "No easy puzzles loaded"; ASSERT_GT(puzzles.size(), 0) << "No " << difficulty << " puzzles loaded";
int solvedCount = 0; int solvedCount = 0;
auto start = std::chrono::high_resolution_clock::now(); auto start = std::chrono::high_resolution_clock::now();
for (size_t i = 0; i < easyPuzzles.size(); ++i) { WFC::WFCStackAllocator allocator{};
auto& sudoku = easyPuzzles[i];
EXPECT_TRUE(sudoku.isValid()) << "Puzzle " << i << " is not valid"; for (size_t i = 0; i < puzzles.size(); ++i) {
Sudoku& sudoku = puzzles[i];
EXPECT_TRUE(sudoku.isValid()) << difficulty << " puzzle " << i << " is not valid";
auto puzzleStart = std::chrono::high_resolution_clock::now(); auto puzzleStart = std::chrono::high_resolution_clock::now();
SudokuSolver::Run(sudoku, true); SudokuSolver::Run(sudoku, allocator);
auto puzzleEnd = std::chrono::high_resolution_clock::now(); auto puzzleEnd = std::chrono::high_resolution_clock::now();
EXPECT_TRUE(sudoku.isSolved()) << "Puzzle " << i << " was not solved"; EXPECT_TRUE(sudoku.isSolved()) << difficulty << " puzzle " << i << " was not solved. Puzzle string: " << sudoku.toString();
if (sudoku.isSolved()) { if (sudoku.isSolved()) {
solvedCount++; solvedCount++;
} }
auto puzzleDuration = std::chrono::duration_cast<std::chrono::milliseconds>(puzzleEnd - puzzleStart); // give progress every max(100, puzzles.size() / 100) puzzles and output percentage complete
if (i < 5) { // Only print timing for first 5 puzzles to avoid spam if (i % std::max<size_t>(100, puzzles.size() / 100) == 0) {
std::cout << "Easy puzzle " << i << " solved in " << puzzleDuration.count() << "ms" << std::endl; std::cout << difficulty << " puzzles: solved " << solvedCount << "/" << puzzles.size()
<< " in " << std::chrono::duration_cast<std::chrono::seconds>(std::chrono::high_resolution_clock::now() - start).count() << " seconds" << std::endl;
std::cout << "Percentage complete: " << static_cast<int>(static_cast<double>(i) / puzzles.size() * 100.0) << "%" << std::endl;
} }
} }
auto end = std::chrono::high_resolution_clock::now(); auto end = std::chrono::high_resolution_clock::now();
auto totalDuration = std::chrono::duration_cast<std::chrono::seconds>(end - start); auto totalDuration = std::chrono::duration_cast<std::chrono::seconds>(end - start);
std::cout << "Easy puzzles: solved " << solvedCount << "/" << easyPuzzles.size() std::cout << difficulty << " puzzles: solved " << solvedCount << "/" << puzzles.size()
<< " in " << totalDuration.count() << " seconds" << std::endl; << " in " << totalDuration.count() << " seconds" << std::endl;
EXPECT_EQ(solvedCount, easyPuzzles.size()) << "Not all easy puzzles were solved"; EXPECT_EQ(solvedCount, puzzles.size()) << "Not all " << difficulty << " puzzles were solved";
} }
TEST_F(SudokuTest, LoadAndSolveMediumPuzzles) // Tests loading and solving puzzles from data files using the unified function
{ TEST_F(SudokuTest, LoadAndSolveEasyPuzzles) {
std::vector<Sudoku> mediumPuzzles = loadPuzzlesFromFile("../data/Sudoku_medium.txt"); testPuzzleSolving("Easy", "../data/Sudoku_easy.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();
SudokuSolver::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<std::chrono::milliseconds>(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<std::chrono::seconds>(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) TEST_F(SudokuTest, LoadAndSolveMediumPuzzles) {
{ testPuzzleSolving("Medium", "../data/Sudoku_medium.txt");
std::vector<Sudoku> hardPuzzles = loadPuzzlesFromFile("../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();
SudokuSolver::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<std::chrono::milliseconds>(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<std::chrono::seconds>(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) TEST_F(SudokuTest, LoadAndSolveHardPuzzles) {
{ testPuzzleSolving("Hard", "../data/Sudoku_hard.txt");
std::vector<Sudoku> diabolicalPuzzles = loadPuzzlesFromFile("../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();
SudokuSolver::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<std::chrono::milliseconds>(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<std::chrono::seconds>(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, LoadAndSolveDiabolicalPuzzles) {
TEST_F(SudokuTest, LoadAndSolveFirstPuzzleFromEachFile) testPuzzleSolving("Diabolical", "../data/Sudoku_diabolical.txt");
{ }
// Test loading and solving the first puzzle from each difficulty file
TEST_F(SudokuTest, LoadAndSolveFirstPuzzleFromEachFile) {
const std::string dataPath = "../data"; const std::string dataPath = "../data";
const std::vector<std::string> files = {"Sudoku_easy.txt", "Sudoku_medium.txt", "Sudoku_hard.txt", "Sudoku_diabolical.txt"}; const std::vector<std::pair<std::string, std::string>> fileTests = {
{"Sudoku_easy.txt", "Easy"},
{"Sudoku_medium.txt", "Medium"},
{"Sudoku_hard.txt", "Hard"},
{"Sudoku_diabolical.txt", "Diabolical"}
};
for (const auto& filename : files) { for (const auto& [filename, difficulty] : fileTests) {
std::string filepath = dataPath + "/" + filename; std::string filepath = dataPath + "/" + filename;
std::ifstream file(filepath); std::ifstream file(filepath);
@@ -459,13 +363,13 @@ TEST_F(SudokuTest, LoadAndSolveFirstPuzzleFromEachFile)
EXPECT_TRUE(puzzle.isValid()) << "Loaded puzzle from " << filename << " is not valid"; EXPECT_TRUE(puzzle.isValid()) << "Loaded puzzle from " << filename << " is not valid";
auto start = std::chrono::high_resolution_clock::now(); auto start = std::chrono::high_resolution_clock::now();
SudokuSolver::Run(puzzle, true); solvePuzzle(puzzle);
auto end = std::chrono::high_resolution_clock::now(); auto end = std::chrono::high_resolution_clock::now();
EXPECT_TRUE(puzzle.isSolved()) << "Failed to solve first puzzle from " << filename; EXPECT_TRUE(puzzle.isSolved()) << "Failed to solve first puzzle from " << filename;
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start); auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
std::cout << "First puzzle from " << filename << " solved in " << duration.count() << "ms" << std::endl; std::cout << "First " << difficulty << " puzzle solved in " << duration.count() << "ms" << std::endl;
} }
} }

Binary file not shown.

View File

@@ -1,188 +0,0 @@
#include "console_renderer.h"
#include "sudoku/sudoku.h"
#include <iostream>
#include <string>
#include <chrono>
#include <thread>
/**
* Sudoku Demo with Real-time Console Rendering
*
* This demo shows how to use the SudokuRenderer to display and animate
* a Sudoku puzzle being solved in real-time without adding new lines.
*/
void demonstrateSudokuRendering() {
std::cout << "=== SUDOKU CONSOLE RENDERING DEMO ===" << std::endl;
std::cout << "This demo will show a Sudoku puzzle and simulate solving it." << std::endl;
std::cout << "Press Enter to continue..." << std::endl;
std::cin.get();
// Clear screen for clean demo
ConsoleRenderer::clearScreen();
// Create a sample Sudoku puzzle
std::string puzzle = "530070000600195000098000060800060003400803001700020006060000280000419005000080079";
Sudoku sudoku(puzzle);
// Create renderer
SudokuRenderer renderer(sudoku);
// Allocate space first (this is crucial!)
std::cout << "Allocating console space..." << std::endl;
ConsoleRenderer::sleep(1000);
renderer.allocateSpace();
// Initial render
std::cout << "Rendering initial puzzle..." << std::endl;
ConsoleRenderer::sleep(500);
renderer.render();
// Wait a moment
ConsoleRenderer::sleep(2000);
// Simulate solving some cells with animation
std::vector<std::tuple<int, int, uint8_t>> moves = {
{0, 2, 1}, {0, 5, 8}, {0, 7, 4}, {0, 8, 2},
{1, 0, 2}, {1, 3, 4}, {1, 6, 7}, {1, 8, 3},
{2, 2, 7}, {2, 4, 3}, {2, 5, 2}, {2, 7, 6},
{3, 1, 9}, {3, 3, 5}, {3, 6, 2}, {3, 8, 7},
{4, 0, 5}, {4, 2, 6}, {4, 6, 9}, {4, 8, 8}
};
renderer.showSolvingProgress("Starting to solve...");
ConsoleRenderer::sleep(1000);
for (size_t i = 0; i < moves.size(); ++i) {
auto [row, col, value] = moves[i];
// Update status
std::string status = "Solving step " + std::to_string(i + 1) + " of " + std::to_string(moves.size());
renderer.showSolvingProgress(status);
// Set the value
sudoku.set(row, col, value);
// Animate the change
renderer.animateCell(row, col, value, 200);
// Small delay between moves
ConsoleRenderer::sleep(300);
}
// Final render
renderer.showSolvingProgress("Partial solution completed!");
ConsoleRenderer::sleep(2000);
// Show final state
if (sudoku.isSolved()) {
renderer.showSolvingProgress("PUZZLE SOLVED!");
} else {
renderer.showSolvingProgress("Partial solution (demo complete)");
}
ConsoleRenderer::sleep(2000);
}
void demonstrateRealTimeUpdates() {
std::cout << "\n=== REAL-TIME UPDATE DEMO ===" << std::endl;
std::cout << "This shows how to update the puzzle without adding lines." << std::endl;
std::cout << "Press Enter to continue..." << std::endl;
std::cin.get();
ConsoleRenderer::clearScreen();
// Create empty sudoku
Sudoku sudoku;
SudokuRenderer renderer(sudoku);
// Allocate and render
renderer.allocateSpace();
renderer.render();
ConsoleRenderer::sleep(1000);
// Add numbers one by one
std::vector<std::tuple<int, int, uint8_t>> sequence = {
{0, 0, 5}, {0, 1, 3}, {0, 4, 7},
{1, 0, 6}, {1, 3, 1}, {1, 4, 9}, {1, 5, 5},
{2, 1, 9}, {2, 2, 8}, {2, 7, 6},
{3, 0, 8}, {3, 4, 6}, {3, 8, 3},
{4, 0, 4}, {4, 3, 8}, {4, 5, 3}, {4, 8, 1}
};
for (const auto& [row, col, value] : sequence) {
renderer.showSolvingProgress("Adding number " + std::to_string(value) +
" at (" + std::to_string(row + 1) + "," + std::to_string(col + 1) + ")");
sudoku.set(row, col, value);
renderer.renderWithHighlight(row, col);
ConsoleRenderer::sleep(800);
renderer.render();
ConsoleRenderer::sleep(200);
}
renderer.showSolvingProgress("Real-time update demo complete!");
ConsoleRenderer::sleep(2000);
}
void showAPIUsage() {
std::cout << "\n=== API USAGE EXAMPLE ===" << std::endl;
std::cout << "Here's how to use the SudokuRenderer API:" << std::endl;
std::cout << std::endl;
std::cout << "// 1. Create a Sudoku puzzle" << std::endl;
std::cout << "Sudoku sudoku(\"530070000600195000...\");" << std::endl;
std::cout << std::endl;
std::cout << "// 2. Create renderer" << std::endl;
std::cout << "SudokuRenderer renderer(sudoku);" << std::endl;
std::cout << std::endl;
std::cout << "// 3. IMPORTANT: Allocate space first!" << std::endl;
std::cout << "renderer.allocateSpace();" << std::endl;
std::cout << std::endl;
std::cout << "// 4. Render initial state" << std::endl;
std::cout << "renderer.render();" << std::endl;
std::cout << std::endl;
std::cout << "// 5. Update puzzle and re-render" << std::endl;
std::cout << "sudoku.set(0, 0, 5);" << std::endl;
std::cout << "renderer.render(); // Updates in place!" << std::endl;
std::cout << std::endl;
std::cout << "// 6. Optional: Use animations" << std::endl;
std::cout << "renderer.animateCell(row, col, value);" << std::endl;
std::cout << "renderer.showSolvingProgress(\"Status\");" << std::endl;
std::cout << std::endl;
std::cout << "Press Enter to continue..." << std::endl;
std::cin.get();
}
int main() {
try {
// Hide cursor for cleaner display
ConsoleRenderer::hideCursor();
// Run demonstrations
showAPIUsage();
demonstrateSudokuRendering();
demonstrateRealTimeUpdates();
// Show cursor again
ConsoleRenderer::showCursor();
std::cout << "\n=== DEMO COMPLETE ===" << std::endl;
std::cout << "The SudokuRenderer provides a clean API for real-time puzzle rendering!" << std::endl;
} catch (const std::exception& e) {
ConsoleRenderer::showCursor();
std::cerr << "Error: " << e.what() << std::endl;
return 1;
}
return 0;
}

View File

@@ -1,33 +0,0 @@
set(EXAMPLE_SOURCES
)
# Create executables for each example
foreach(EXAMPLE_SOURCE ${EXAMPLE_SOURCES})
get_filename_component(EXAMPLE_NAME ${EXAMPLE_SOURCE} NAME_WE)
add_executable(${EXAMPLE_NAME} ${EXAMPLE_SOURCE})
target_link_libraries(${EXAMPLE_NAME}
PRIVATE
nd-wfc
# SDL3::SDL3 # Will be enabled when SDL3 is needed
)
target_include_directories(${EXAMPLE_NAME}
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../include
)
set_target_properties(${EXAMPLE_NAME} PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/examples
)
endforeach()
# Copy resources if they exist
if(EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/resources)
add_custom_target(copy-example-resources
COMMAND ${CMAKE_COMMAND} -E copy_directory
${CMAKE_CURRENT_SOURCE_DIR}/resources
${CMAKE_BINARY_DIR}/examples/resources
)
# Dependencies for resource copying would go here if needed
endif()

View File

@@ -18,8 +18,12 @@
namespace WFC { namespace WFC {
inline constexpr void constexpr_assert(bool condition, const char* message = "") {
if (!condition) throw message;
}
inline int FindNthSetBit(size_t num, int n) { inline int FindNthSetBit(size_t num, int n) {
assert(n < std::popcount(num) && "index is out of range"); constexpr_assert(n < std::popcount(num), "index is out of range");
int bitCount = 0; int bitCount = 0;
while (num) { while (num) {
if (bitCount == n) { if (bitCount == n) {
@@ -111,7 +115,7 @@ public:
} }
static VarT GetValue(size_t index) { static VarT GetValue(size_t index) {
assert(index < ValuesRegisteredAmount); constexpr_assert(index < ValuesRegisteredAmount);
return GetAllValues()[index]; return GetAllValues()[index];
} }
@@ -374,34 +378,40 @@ public:
WFC() = delete; // dont make an instance of this class, only use the static methods. WFC() = delete; // dont make an instance of this class, only use the static methods.
public: public:
static bool Run(WorldT& world, bool propagateInitialValues = true, WFCStackAllocator* allocator = nullptr) static bool Run(WorldT& world, uint32_t seed = std::random_device{}())
{ {
//std::mt19937 random{ 212 }; WFCStackAllocator allocator{};
std::mt19937 random{ std::random_device{}() }; return Run(world, allocator, seed);
}
static bool Run(WorldT& world, WFCStackAllocator& allocator, uint32_t seed = std::random_device{}())
{
allocator.reset();
constexpr_assert(allocator.getUsed() == 0, "Allocator must be empty");
size_t iterations = 0; size_t iterations = 0;
if (!allocator) auto random = std::mt19937{ seed };
SolverState state
{ {
WFCStackAllocator newAllocator{}; world,
SolverState state(world, ConstrainerFunctionMapT::size(), random, newAllocator, iterations); ConstrainerFunctionMapT::size(),
return Run(state, propagateInitialValues); random,
} allocator,
else iterations
{ };
SolverState state(world, ConstrainerFunctionMapT::size(), random, *allocator, iterations); return Run(state);
return Run(state, propagateInitialValues);
} allocator.reset();
constexpr_assert(allocator.getUsed() == 0, "Allocator must be empty");
} }
/** /**
* @brief Run the WFC algorithm to generate a solution * @brief Run the WFC algorithm to generate a solution
* @return true if a solution was found, false if contradiction occurred * @return true if a solution was found, false if contradiction occurred
*/ */
static bool Run(SolverState& state, bool propagateInitialValues = true) static bool Run(SolverState& state)
{ {
if (propagateInitialValues) PropogateInitialValues(state);
{
PropogateInitialValues(state);
}
if (RunLoop(state)) { if (RunLoop(state)) {
@@ -413,7 +423,7 @@ public:
static bool RunLoop(SolverState& state) static bool RunLoop(SolverState& state)
{ {
for (; state.iterations < 1024; ++state.iterations) for (; state.iterations < 1024 * 8; ++state.iterations)
{ {
if (!Propagate(state)) if (!Propagate(state))
return false; return false;
@@ -474,9 +484,9 @@ public:
private: private:
static void CollapseCell(SolverState& state, size_t cellId, uint16_t value) static void CollapseCell(SolverState& state, size_t cellId, uint16_t value)
{ {
assert(!state.wave.IsCollapsed(cellId) || state.wave.GetMask(cellId) == (1 << value)); constexpr_assert(!state.wave.IsCollapsed(cellId) || state.wave.GetMask(cellId) == (1 << value));
state.wave.Collapse(cellId, 1 << value); state.wave.Collapse(cellId, 1 << value);
assert(state.wave.IsCollapsed(cellId)); constexpr_assert(state.wave.IsCollapsed(cellId));
if constexpr (CallbacksT::HasCellCollapsedCallback()) if constexpr (CallbacksT::HasCellCollapsedCallback())
{ {
@@ -487,7 +497,7 @@ private:
static bool Branch(SolverState& state) static bool Branch(SolverState& state)
{ {
assert(state.propagationQueue.empty()); constexpr_assert(state.propagationQueue.empty());
// Find cell with minimum entropy > 1 // Find cell with minimum entropy > 1
size_t minEntropyCell = static_cast<size_t>(-1); size_t minEntropyCell = static_cast<size_t>(-1);
@@ -502,7 +512,7 @@ private:
} }
if (minEntropyCell == static_cast<size_t>(-1)) return false; if (minEntropyCell == static_cast<size_t>(-1)) return false;
assert(!state.wave.IsCollapsed(minEntropyCell)); constexpr_assert(!state.wave.IsCollapsed(minEntropyCell));
// create a list of possible values // create a list of possible values
uint16_t availableValues = static_cast<uint16_t>(state.wave.Entropy(minEntropyCell)); uint16_t availableValues = static_cast<uint16_t>(state.wave.Entropy(minEntropyCell));
@@ -511,10 +521,10 @@ private:
for (size_t i = 0; i < availableValues; ++i) for (size_t i = 0; i < availableValues; ++i)
{ {
uint16_t index = static_cast<uint16_t>(std::countr_zero(mask)); // get the index of the lowest set bit uint16_t index = static_cast<uint16_t>(std::countr_zero(mask)); // get the index of the lowest set bit
assert(index < VariableIDMapT::ValuesRegisteredAmount && "Possible value went outside bounds"); constexpr_assert(index < VariableIDMapT::ValuesRegisteredAmount, "Possible value went outside bounds");
possibleValues[i] = index; possibleValues[i] = index;
assert(((mask & (1 << index)) != 0) && "Possible value was not set"); constexpr_assert(((mask & (1 << index)) != 0), "Possible value was not set");
mask = mask & (mask - 1); // turn off lowest set bit mask = mask & (mask - 1); // turn off lowest set bit
} }
@@ -543,9 +553,9 @@ private:
} }
// remove the failure state from the wave // remove the failure state from the wave
assert((state.wave.GetMask(minEntropyCell) & (1 << selectedValue)) != 0 && "Possible value was not set"); constexpr_assert((state.wave.GetMask(minEntropyCell) & (1 << selectedValue)) != 0, "Possible value was not set");
state.wave.Collapse(minEntropyCell, ~(1 << selectedValue)); state.wave.Collapse(minEntropyCell, ~(1 << selectedValue));
assert((state.wave.GetMask(minEntropyCell) & (1 << selectedValue)) == 0 && "Wave was not collapsed correctly"); constexpr_assert((state.wave.GetMask(minEntropyCell) & (1 << selectedValue)) == 0, "Wave was not collapsed correctly");
// swap replacement value with the last value // swap replacement value with the last value
std::swap(possibleValues[randomIndex], possibleValues[--availableValues]); std::swap(possibleValues[randomIndex], possibleValues[--availableValues]);
@@ -563,7 +573,7 @@ private:
if (state.wave.IsContradicted(cellId)) return false; if (state.wave.IsContradicted(cellId)) return false;
assert(state.wave.IsCollapsed(cellId) && "Cell was not collapsed"); constexpr_assert(state.wave.IsCollapsed(cellId), "Cell was not collapsed");
uint16_t variableID = state.wave.GetVariableID(cellId); uint16_t variableID = state.wave.GetVariableID(cellId);
Constrainer<VariableIDMapT> constrainer(state.wave, state.propagationQueue); Constrainer<VariableIDMapT> constrainer(state.wave, state.propagationQueue);

View File

@@ -31,7 +31,7 @@ target_link_libraries(nd-wfc-tests
# Set C++ standard # Set C++ standard
set_target_properties(nd-wfc-tests PROPERTIES set_target_properties(nd-wfc-tests PROPERTIES
CXX_STANDARD 17 CXX_STANDARD 20
CXX_STANDARD_REQUIRED ON CXX_STANDARD_REQUIRED ON
) )