diff --git a/demos/CMakeLists.txt b/demos/CMakeLists.txt deleted file mode 100644 index 96e8170..0000000 --- a/demos/CMakeLists.txt +++ /dev/null @@ -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 -) diff --git a/demos/README.md b/demos/README.md deleted file mode 100644 index e89050e..0000000 --- a/demos/README.md +++ /dev/null @@ -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>& 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& 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! diff --git a/demos/console_renderer.cpp b/demos/console_renderer.cpp deleted file mode 100644 index 54ada16..0000000 --- a/demos/console_renderer.cpp +++ /dev/null @@ -1,509 +0,0 @@ -#include "console_renderer.h" -#include "sudoku/sudoku.h" -#include "nonogram/nonogram.h" -#include -#include - -// ANSI escape codes for cursor control -#ifdef _WIN32 - #include - #include -#else - #include - #include -#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(text.length()) >= width) return text; - int padding = (width - static_cast(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(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(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('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(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>& 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(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>* 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(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(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(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(hints.size())); - - if (hint_index >= 0 && hint_index < static_cast(hints.size())) { - oss << static_cast(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(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(hints.size())); - } - - return 2 + max_col_hints + 1 + static_cast(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& 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(current) / total; - int filled = static_cast(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(percentage * 100) << "%"; - std::cout.flush(); -} diff --git a/demos/console_renderer.h b/demos/console_renderer.h deleted file mode 100644 index d3c2262..0000000 --- a/demos/console_renderer.h +++ /dev/null @@ -1,130 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -/** - * 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>& state); - void showSolvingProgress(const std::string& status = ""); - -private: - const class Nonogram& nonogram_; - - // Rendering helpers - std::string formatNonogramLine(int row, const std::vector>* 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& lines, int delay_ms = 100); - static void progressBar(const std::string& label, int current, int total, int width = 30); -}; diff --git a/demos/sudoku/CMakeLists.txt b/demos/sudoku/CMakeLists.txt index f0a2c1c..78bdd8a 100644 --- a/demos/sudoku/CMakeLists.txt +++ b/demos/sudoku/CMakeLists.txt @@ -48,11 +48,7 @@ if(MSVC) set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) endif() -# Create the main executable -add_executable(sudoku_demo - main.cpp - sudoku.cpp -) + # Create WFC demo executable add_executable(sudoku_wfc_demo @@ -60,36 +56,19 @@ 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 -) -# 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(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) - target_compile_options(sudoku_demo PRIVATE $<$:/MDd> $<$:/MD>) target_compile_options(sudoku_wfc_demo PRIVATE $<$:/MDd> $<$:/MD>) - target_compile_options(analyze_failing_puzzles PRIVATE $<$:/MDd> $<$:/MD>) - target_compile_options(debug_failing_puzzles PRIVATE $<$:/MDd> $<$:/MD>) 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_target_properties(sudoku_wfc_demo PROPERTIES @@ -98,38 +77,19 @@ 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 (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(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 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() @@ -193,9 +153,9 @@ endif() # Installation (optional) 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) - 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() - install(TARGETS sudoku_demo sudoku_wfc_demo analyze_failing_puzzles debug_failing_puzzles DESTINATION bin) + install(TARGETS sudoku_wfc_demo DESTINATION bin) endif() diff --git a/demos/sudoku/analyze_failing_puzzles.cpp b/demos/sudoku/analyze_failing_puzzles.cpp deleted file mode 100644 index c14ef11..0000000 --- a/demos/sudoku/analyze_failing_puzzles.cpp +++ /dev/null @@ -1,158 +0,0 @@ -#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() { - - // 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 deleted file mode 100644 index 01ba15b..0000000 --- a/demos/sudoku/debug_failing_puzzles.cpp +++ /dev/null @@ -1,165 +0,0 @@ -#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 - - // 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/main.cpp b/demos/sudoku/main.cpp deleted file mode 100644 index 1f0ee56..0000000 --- a/demos/sudoku/main.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "sudoku.h" -#include - -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; -} diff --git a/demos/sudoku/sudoku.cpp b/demos/sudoku/sudoku.cpp index 045b3ba..a36cdc0 100644 --- a/demos/sudoku/sudoku.cpp +++ b/demos/sudoku/sudoku.cpp @@ -6,39 +6,6 @@ #include #include -Sudoku::Sudoku() { - clear(); -} - -Sudoku::Sudoku(const std::string& puzzle_str) { - clear(); - loadFromString(puzzle_str); -} - -bool Sudoku::loadFromString(const std::string& puzzle_str) { - if (puzzle_str.length() != 81) { - return false; - } - - clear(); - - for (int i = 0; i < 81; ++i) { - char c = puzzle_str[i]; - if (c >= '1' && c <= '9') { - int row = i / 9; - int col = i % 9; - uint8_t value = c - '0'; - if (!set(row, col, value)) { - return false; // Invalid move - } - } else if (c != '0' && c != '.') { - return false; // Invalid character - } - } - - return true; -} - bool Sudoku::loadFromFile(const std::string& filename) { std::ifstream file(filename); if (!file.is_open()) { @@ -60,68 +27,11 @@ bool Sudoku::loadFromFile(const std::string& filename) { return loadFromString(puzzle_str); } - - -void Sudoku::clear() { - board_.clear(); -} - -bool Sudoku::isValid() const { - // Check rows for duplicates using bitset for efficiency - for (int row = 0; row < 9; ++row) { - std::bitset<10> seen; // bits 1-9 track values 1-9 - for (int col = 0; col < 9; ++col) { - uint8_t value = get(row, col); - if (value != 0) { - if (seen[value]) return false; // Duplicate found - seen.set(value); - } - } - } - - // Check columns for duplicates - for (int col = 0; col < 9; ++col) { - std::bitset<10> seen; - for (int row = 0; row < 9; ++row) { - uint8_t value = get(row, col); - if (value != 0) { - if (seen[value]) return false; // Duplicate found - seen.set(value); - } - } - } - - // Check boxes for duplicates - for (int box = 0; box < 9; ++box) { - std::bitset<10> seen; - int startRow = (box / 3) * 3; - int startCol = (box % 3) * 3; - for (int row = 0; row < 3; ++row) { - for (int col = 0; col < 3; ++col) { - uint8_t value = get(startRow + row, startCol + col); - if (value != 0) { - if (seen[value]) return false; // Duplicate found - seen.set(value); - } - } - } - } - - return true; -} - -bool Sudoku::isSolved() const { - for (int i = 0; i < 81; ++i) { - if (board_.get(i) == 0) return false; - } - return isValid(); -} - -void Sudoku::print() const { +void Sudoku::print(char emptyVal) const { for (int row = 0; row < 9; ++row) { for (int col = 0; col < 9; ++col) { uint8_t value = get(row, col); - std::cout << (value == 0 ? '.' : static_cast('0' + value)); + std::cout << (value == 0 ? emptyVal : static_cast('0' + value)); if (col < 8) 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; result.reserve(81); for (int i = 0; i < 81; ++i) { uint8_t cell = board_.get(i); - result += (cell == 0 ? '.' : static_cast('0' + cell)); + result += (cell == 0 ? emptyVal : static_cast('0' + cell)); } return result; } diff --git a/demos/sudoku/sudoku.h b/demos/sudoku/sudoku.h index a560138..b15bd8d 100644 --- a/demos/sudoku/sudoku.h +++ b/demos/sudoku/sudoku.h @@ -5,8 +5,12 @@ #include #include #include -#include #include +#include +#include +#include +#include + #include // 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) class SudokuBoardStorage { public: - std::array data; + std::array data {}; // Get 4-bit value at position (0-80) // Each byte contains 2 cells: [cell0(4bits)][cell1(4bits)] // Ultra-fast: only bitwise operations, no divide/modulo! // Optimization: pos >> 1 instead of pos / 2 // Optimization: (pos & 1) << 2 instead of (pos % 2) * 4 - uint8_t get(int pos) const { + constexpr inline uint8_t get(int pos) const { int byteIndex = pos >> 1; // pos / 2 using right shift // Precomputed shift amounts: 4 for even positions, 0 for odd positions @@ -34,7 +38,7 @@ public: uint8_t result = (data[byteIndex] >> shiftAmount) & 0xF; // Debug assertion: ensure result is in valid range - assert(result >= 0 && result <= 9 && "Sudoku cell value must be between 0-9"); + WFC::constexpr_assert(result >= 0 && result <= 9, "Sudoku cell value must be between 0-9"); return result; } @@ -43,9 +47,9 @@ public: // Ultra-fast: only bitwise operations, no divide/modulo! // Optimization: pos >> 1 instead of pos / 2 // Optimization: (pos & 1) << 2 instead of (pos % 2) * 4 - void set(int pos, uint8_t value) { + constexpr inline void set(int pos, uint8_t value) { // Assert that value is in valid Sudoku range (0-9) - assert(value >= 0 && value <= 9 && "Sudoku cell value must be between 0-9"); + WFC::constexpr_assert(value >= 0 && value <= 9, "Sudoku cell value must be between 0-9"); int byteIndex = pos >> 1; // pos / 2 using right shift @@ -59,7 +63,7 @@ public: data[byteIndex] = (data[byteIndex] & mask) | (value << shiftAmount); } - void clear() { + constexpr void clear() { data.fill(0); } }; @@ -67,23 +71,49 @@ public: // Ultra-memory-efficient Sudoku class: exactly 41 bytes class Sudoku { public: - Sudoku(); - explicit Sudoku(const std::string& puzzle_str); + constexpr Sudoku() = default; + constexpr explicit Sudoku(std::string_view puzzle_str) + { + loadFromString(puzzle_str); + } // 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); // Board access (inlined for performance) - inline uint8_t get(int row, int col) const { - assert((row >= 0 && row < 9 && col >= 0 && col < 9) && + constexpr inline uint8_t get(int row, int col) const { + WFC::constexpr_assert((row >= 0 && row < 9 && col >= 0 && col < 9), "Sudoku::get() called with invalid position - row and col must be 0-8"); int linearIndex = getLinearIndex(row, col); return board_.get(linearIndex); } - inline bool set(int row, int col, uint8_t value) { - assert((row >= 0 && row < 9 && col >= 0 && col < 9) && + constexpr inline bool set(int row, int col, uint8_t value) { + WFC::constexpr_assert((row >= 0 && row < 9 && col >= 0 && col < 9), "Sudoku::set() called with invalid position - row and col must be 0-8"); // Keep value validation as runtime check since it's about valid Sudoku numbers @@ -104,14 +134,64 @@ public: return true; } - void clear(); + constexpr void clear() { + board_.clear(); + } // Validation - bool isValid() const; - bool isSolved() const; + constexpr bool 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; + } + + 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()) - 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; return !hasRowConflictExcluding(row, col, value) && !hasColConflictExcluding(col, row, value) && @@ -119,8 +199,8 @@ public: } // Utility - void print() const; - std::string toString() const; + void print(char emptyVal = '.') const; + std::string toString(char emptyVal = '.') const; // Convert to standard board format for external use std::array getBoard() const; @@ -129,35 +209,35 @@ private: SudokuBoardStorage board_; // 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; } - inline int getBoxIndex(int row, int col) const { + constexpr inline int getBoxIndex(int row, int col) const { 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; } // Validation helpers (inlined for performance) // Uses std::bitset<10> for efficient duplicate detection instead of arrays - inline bool hasRowConflict(int row, uint8_t value) const { + constexpr inline bool hasRowConflict(int row, uint8_t value) const { for (int col = 0; col < 9; ++col) { if (get(row, col) == value) return true; } return false; } - inline bool hasColConflict(int col, uint8_t value) const { + constexpr inline bool hasColConflict(int col, uint8_t value) const { for (int row = 0; row < 9; ++row) { if (get(row, col) == value) return true; } return false; } - inline bool hasBoxConflict(int box, uint8_t value) const { + constexpr inline bool hasBoxConflict(int box, uint8_t value) const { int startRow = (box / 3) * 3; int startCol = (box % 3) * 3; for (int row = 0; row < 3; ++row) { @@ -169,21 +249,21 @@ private: } // 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) { if (col != excludeCol && get(row, col) == value) return true; } 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) { if (row != excludeRow && get(row, col) == value) return true; } 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 startCol = (box % 3) * 3; for (int row = 0; row < 3; ++row) { @@ -199,15 +279,15 @@ private: public: // WFC Support using ValueType = uint8_t; - ValueType getValue(size_t index) const { + constexpr inline ValueType getValue(size_t index) const { return board_.get(static_cast(index)); } - void setValue(size_t index, ValueType value) { + constexpr inline void setValue(size_t index, ValueType value) { board_.set(static_cast(index), value); } - constexpr size_t size() const { + constexpr inline size_t size() const { return 81; } }; @@ -234,8 +314,6 @@ public: private: static bool parseLine(const std::string& line, std::array& board); }; - - using SudokuSolverBuilder = WFC::Builder ::DefineIDs<1, 2, 3, 4, 5, 6, 7, 8, 9> ::DefineConstrainer val, auto& constrainer) { @@ -268,4 +346,5 @@ using SudokuSolverBuilder = WFC::Builder }), 1, 2, 3, 4, 5, 6, 7, 8, 9>; -using SudokuSolver = SudokuSolverBuilder::Build; \ No newline at end of file +using SudokuSolver = SudokuSolverBuilder::Build; + diff --git a/demos/sudoku/sudoku_wfc.cpp b/demos/sudoku/sudoku_wfc.cpp index 9f61975..2bd8827 100644 --- a/demos/sudoku/sudoku_wfc.cpp +++ b/demos/sudoku/sudoku_wfc.cpp @@ -58,11 +58,16 @@ using SudokuSolverCallback = SudokuSolverBuilder::SetCellCollapsedCallback ::Build; +Sudoku GetWorldConsteval() +{ + return Sudoku{ "6......3.......7....7463....7.8...2.4...9...1.9...7.8....9851....6.......1......9" }; +} + int main() { std::cout << "Running Sudoku WFC" << std::endl; - Sudoku sudokuWorld{ "040280030010006007609070008000092000900000004000740000500020803400800010070035090" }; + Sudoku sudokuWorld = GetWorldConsteval(); bool success = SudokuSolverCallback::Run(sudokuWorld, true); @@ -84,5 +89,5 @@ int main() if (y == 2 || y == 5) std::cout << "------+-------+------" << std::endl; } - + return (success && solved) ? 0 : 1; } \ No newline at end of file diff --git a/demos/sudoku/sudoku_wfc_demo b/demos/sudoku/sudoku_wfc_demo deleted file mode 100755 index 090a8d0..0000000 Binary files a/demos/sudoku/sudoku_wfc_demo and /dev/null differ diff --git a/demos/sudoku/sudoku_wfc_manual b/demos/sudoku/sudoku_wfc_manual deleted file mode 100755 index 090a8d0..0000000 Binary files a/demos/sudoku/sudoku_wfc_manual and /dev/null differ diff --git a/demos/sudoku/test_sudoku.cpp b/demos/sudoku/test_sudoku.cpp index 54a9f50..8d238ca 100644 --- a/demos/sudoku/test_sudoku.cpp +++ b/demos/sudoku/test_sudoku.cpp @@ -4,22 +4,17 @@ #include #include #include +#include +#include +#include -// Forward declaration for helper function +// Forward declarations for helper functions std::vector loadPuzzlesFromFile(const std::string& filename); +void testPuzzleSolving(const std::string& difficulty, const std::string& filename); // Test fixture for Sudoku tests class SudokuTest : public ::testing::Test { protected: - - void SetUp() override { - // Common test setup - } - - void TearDown() override { - // Common test cleanup - } - // Helper function to create a solved Sudoku Sudoku createSolvedSudoku() { Sudoku sudoku; @@ -36,9 +31,9 @@ protected: return sudoku; } - Sudoku SolvePuzzle(Sudoku& sudoku) { + // Helper function to solve a puzzle using WFC + void solvePuzzle(Sudoku& sudoku) { SudokuSolver::Run(sudoku, true); - return sudoku; } }; @@ -246,7 +241,7 @@ TEST_F(SudokuTest, PerformanceSetOperations) { int row = i % 9; int col = (i / 9) % 9; int value = (i % 9) + 1; - sudoku.set(row, col, value); + sudoku.set(row, col, static_cast(value)); } auto end = std::chrono::high_resolution_clock::now(); @@ -272,173 +267,82 @@ TEST_F(SudokuTest, EdgeCases) { EXPECT_EQ(sudoku.get(8, 8), 4); } -TEST_F(SudokuTest, WFCIntegration) -{ - auto sudoku = createEasyPuzzle(); - SudokuSolver::Run(sudoku, true); +TEST_F(SudokuTest, WFCIntegration) { + Sudoku sudoku = createEasyPuzzle(); + solvePuzzle(sudoku); EXPECT_TRUE(sudoku.isSolved()); } -// Tests loading and solving puzzles from data files -TEST_F(SudokuTest, LoadAndSolveEasyPuzzles) -{ - std::vector easyPuzzles = loadPuzzlesFromFile("../data/Sudoku_easy.txt"); +// Unified function to test puzzle solving for different difficulty levels +void testPuzzleSolving(const std::string& difficulty, const std::string& filename) { + std::vector puzzles = loadPuzzlesFromFile(filename); - ASSERT_GT(easyPuzzles.size(), 0) << "No easy puzzles loaded"; + ASSERT_GT(puzzles.size(), 0) << "No " << difficulty << " 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"; + WFC::WFCStackAllocator allocator{}; + + 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(); - SudokuSolver::Run(sudoku, true); + SudokuSolver::Run(sudoku, allocator); 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()) { 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; + // give progress every max(100, puzzles.size() / 100) puzzles and output percentage complete + if (i % std::max(100, puzzles.size() / 100) == 0) { + std::cout << difficulty << " puzzles: solved " << solvedCount << "/" << puzzles.size() + << " in " << std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count() << " seconds" << std::endl; + std::cout << "Percentage complete: " << static_cast(static_cast(i) / puzzles.size() * 100.0) << "%" << 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() + std::cout << difficulty << " puzzles: solved " << solvedCount << "/" << puzzles.size() << " 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) -{ - std::vector mediumPuzzles = loadPuzzlesFromFile("../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(); - 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(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"; +// Tests loading and solving puzzles from data files using the unified function +TEST_F(SudokuTest, LoadAndSolveEasyPuzzles) { + testPuzzleSolving("Easy", "../data/Sudoku_easy.txt"); } -TEST_F(SudokuTest, LoadAndSolveHardPuzzles) -{ - std::vector 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(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, LoadAndSolveMediumPuzzles) { + testPuzzleSolving("Medium", "../data/Sudoku_medium.txt"); } -TEST_F(SudokuTest, LoadAndSolveDiabolicalPuzzles) -{ - std::vector 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(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_F(SudokuTest, LoadAndSolveHardPuzzles) { + testPuzzleSolving("Hard", "../data/Sudoku_hard.txt"); } -// Test loading a single puzzle from each difficulty file -TEST_F(SudokuTest, LoadAndSolveFirstPuzzleFromEachFile) -{ +TEST_F(SudokuTest, LoadAndSolveDiabolicalPuzzles) { + 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::vector files = {"Sudoku_easy.txt", "Sudoku_medium.txt", "Sudoku_hard.txt", "Sudoku_diabolical.txt"}; + const std::vector> 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::ifstream file(filepath); @@ -459,13 +363,13 @@ TEST_F(SudokuTest, LoadAndSolveFirstPuzzleFromEachFile) EXPECT_TRUE(puzzle.isValid()) << "Loaded puzzle from " << filename << " is not valid"; auto start = std::chrono::high_resolution_clock::now(); - SudokuSolver::Run(puzzle, true); + solvePuzzle(puzzle); 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; + std::cout << "First " << difficulty << " puzzle solved in " << duration.count() << "ms" << std::endl; } } diff --git a/demos/sudoku_demo_renderer b/demos/sudoku_demo_renderer deleted file mode 100755 index 015743c..0000000 Binary files a/demos/sudoku_demo_renderer and /dev/null differ diff --git a/demos/sudoku_demo_renderer.cpp b/demos/sudoku_demo_renderer.cpp deleted file mode 100644 index e08575a..0000000 --- a/demos/sudoku_demo_renderer.cpp +++ /dev/null @@ -1,188 +0,0 @@ -#include "console_renderer.h" -#include "sudoku/sudoku.h" -#include -#include -#include -#include - -/** - * 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> 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> 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; -} diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt deleted file mode 100644 index 293a474..0000000 --- a/examples/CMakeLists.txt +++ /dev/null @@ -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() diff --git a/include/nd-wfc/wfc.hpp b/include/nd-wfc/wfc.hpp index b6fac05..f90042b 100644 --- a/include/nd-wfc/wfc.hpp +++ b/include/nd-wfc/wfc.hpp @@ -18,8 +18,12 @@ namespace WFC { +inline constexpr void constexpr_assert(bool condition, const char* message = "") { + if (!condition) throw message; +} + 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; while (num) { if (bitCount == n) { @@ -111,7 +115,7 @@ public: } static VarT GetValue(size_t index) { - assert(index < ValuesRegisteredAmount); + constexpr_assert(index < ValuesRegisteredAmount); return GetAllValues()[index]; } @@ -374,34 +378,40 @@ public: WFC() = delete; // dont make an instance of this class, only use the static methods. 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 }; - std::mt19937 random{ std::random_device{}() }; + WFCStackAllocator allocator{}; + 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; - if (!allocator) + auto random = std::mt19937{ seed }; + SolverState state { - WFCStackAllocator newAllocator{}; - SolverState state(world, ConstrainerFunctionMapT::size(), random, newAllocator, iterations); - return Run(state, propagateInitialValues); - } - else - { - SolverState state(world, ConstrainerFunctionMapT::size(), random, *allocator, iterations); - return Run(state, propagateInitialValues); - } + world, + ConstrainerFunctionMapT::size(), + random, + allocator, + iterations + }; + return Run(state); + + allocator.reset(); + constexpr_assert(allocator.getUsed() == 0, "Allocator must be empty"); } /** * @brief Run the WFC algorithm to generate a solution * @return true if a solution was found, false if contradiction occurred */ - static bool Run(SolverState& state, bool propagateInitialValues = true) + static bool Run(SolverState& state) { - if (propagateInitialValues) - { - PropogateInitialValues(state); - } + PropogateInitialValues(state); if (RunLoop(state)) { @@ -413,7 +423,7 @@ public: static bool RunLoop(SolverState& state) { - for (; state.iterations < 1024; ++state.iterations) + for (; state.iterations < 1024 * 8; ++state.iterations) { if (!Propagate(state)) return false; @@ -474,9 +484,9 @@ public: private: 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); - assert(state.wave.IsCollapsed(cellId)); + constexpr_assert(state.wave.IsCollapsed(cellId)); if constexpr (CallbacksT::HasCellCollapsedCallback()) { @@ -487,7 +497,7 @@ private: static bool Branch(SolverState& state) { - assert(state.propagationQueue.empty()); + constexpr_assert(state.propagationQueue.empty()); // Find cell with minimum entropy > 1 size_t minEntropyCell = static_cast(-1); @@ -502,7 +512,7 @@ private: } if (minEntropyCell == static_cast(-1)) return false; - assert(!state.wave.IsCollapsed(minEntropyCell)); + constexpr_assert(!state.wave.IsCollapsed(minEntropyCell)); // create a list of possible values uint16_t availableValues = static_cast(state.wave.Entropy(minEntropyCell)); @@ -511,10 +521,10 @@ private: for (size_t i = 0; i < availableValues; ++i) { uint16_t index = static_cast(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; - 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 } @@ -543,9 +553,9 @@ private: } // 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)); - 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 std::swap(possibleValues[randomIndex], possibleValues[--availableValues]); @@ -563,7 +573,7 @@ private: 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); Constrainer constrainer(state.wave, state.propagationQueue); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bd22ae3..2a40345 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -31,7 +31,7 @@ target_link_libraries(nd-wfc-tests # Set C++ standard set_target_properties(nd-wfc-tests PROPERTIES - CXX_STANDARD 17 + CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON )