From e9a298e9ef35220c579cd0b95f446454fc59460b Mon Sep 17 00:00:00 2001 From: Connor De Meyer Date: Mon, 1 Sep 2025 09:15:35 +0900 Subject: [PATCH] fixed tests --- demos/CMakeLists.txt | 24 -- demos/README.md | 267 ------------ demos/console_renderer.cpp | 509 ----------------------- demos/console_renderer.h | 130 ------ demos/sudoku/CMakeLists.txt | 62 +-- demos/sudoku/analyze_failing_puzzles.cpp | 158 ------- demos/sudoku/debug_failing_puzzles.cpp | 165 -------- demos/sudoku/main.cpp | 19 - demos/sudoku/sudoku.cpp | 98 +---- demos/sudoku/sudoku.h | 149 +++++-- demos/sudoku/sudoku_wfc.cpp | 9 +- demos/sudoku/sudoku_wfc_demo | Bin 17312 -> 0 bytes demos/sudoku/sudoku_wfc_manual | Bin 17312 -> 0 bytes demos/sudoku/test_sudoku.cpp | 198 +++------ demos/sudoku_demo_renderer | Bin 167360 -> 0 bytes demos/sudoku_demo_renderer.cpp | 188 --------- examples/CMakeLists.txt | 33 -- include/nd-wfc/wfc.hpp | 70 ++-- tests/CMakeLists.txt | 2 +- 19 files changed, 228 insertions(+), 1853 deletions(-) delete mode 100644 demos/CMakeLists.txt delete mode 100644 demos/README.md delete mode 100644 demos/console_renderer.cpp delete mode 100644 demos/console_renderer.h delete mode 100644 demos/sudoku/analyze_failing_puzzles.cpp delete mode 100644 demos/sudoku/debug_failing_puzzles.cpp delete mode 100644 demos/sudoku/main.cpp delete mode 100755 demos/sudoku/sudoku_wfc_demo delete mode 100755 demos/sudoku/sudoku_wfc_manual delete mode 100755 demos/sudoku_demo_renderer delete mode 100644 demos/sudoku_demo_renderer.cpp delete mode 100644 examples/CMakeLists.txt 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 090a8d0f030d168720694bce21453ef5b7d8120e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17312 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 #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 015743c97cd94a62dd5b476037670f3248b5e246..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 167360 zcmeFadtg+>^#{HIR)T_w4-~CRq*_4*!$TzEBPiD{N;Cp#@fl+XkT#H*Yyho6G{&-B z7x6*G`a-KMT5Zu*ztIYYhitS~c2pOu z+J2_++kWBy&ei&Kz8!s%NB$e9^%?xmM;$-Tcd*@T-7fe&Q8f!W-|T~*ssD)1*pvQ> zb-VTvO0MAdiP~Pyx6|K+Xs1=C82FmMnoMYl?k|%a3N=yZTX7W%om5qM<=8PNRm~V) zRXMkQ!SDs+#tt7lW<*`}h*8or`O3pT`qcN&FA2dcEre+`hI%bhWrrT?4%YR=@4CFt z=1-n~>YATyd!*w2)pxyhz%9dv5QluT4RQD(J#BKgJPTLE5!SPA?R9kMv_iu8`7!>r zRlG81=VLGJto?c4UdPVqfA*N$PQP*B^8RNFLWVyd!4D5iQ+L*Y^zZ=?CLR7~2c?Ja z%s{6gL%(qdosRx|2%QeTDJMOAY5(-_S2OVQUtl{lN@& z-O8Jz4NIE*JGVoKC!EayB0N+0Y{TDOn|C|i{ zzMKJGg+P>!|7SAvJ1K+yS7(5?WuX6g2Kb*c_`{JI#!F8IK0nAn|Huq>UzEX5mSoWL zWAKD@dOIpZ{5(Cw_}!Wz9@b`%>#Gd?Zq1N4;@E(86qGSGiI1N_bm^T;_F?6ot4U4~}RXYUOD z*`C4A`(=nHP(vsW|AL77lB+zB-}69+@D&dz z_#pKa`b5Up5uug`6(Co`J28%iAluwIpRGe7OT$Z{e9~E@={xwL($ZOTs^^y0MaycV zrKO?LNz;l-XH?Wy%&M%5R@6=_I=iZRZpE~+E2}D0UHY1f&z~9{Ip)f;y2|p>%9(Z1 z+KRF{lga@bSw6e0wlrE>RvE3ERBl;kkF=(o2jo*qOUoB57&&s}DAfo}D(B8ZlaZxU z%ZO;9@#xY?)pey8)Ktu!Q#}LyvzLqwQ~FYLKxArZd3DXzrR1c}DlRWRuYBq$ko4lI z(W<&hX?UE%t`%F=jwsVgt*IxMmXn9`zC9CTt19Nsiq5v?Nw1^wOG~4(Ypds%DsX8< zZEbZe*xEztl?$vnZ2Il_I;9lGE{m2yD|<#@Jv?El*G5Q=|Pc@n1Z!{1Tjc@O1825C3 z(?`Eu=bFIO=$MMRGpf@1Rc8H-rP&v5bBg>jMt#s-*xWlOY znbo!P%W7x1ZdMvCo3)4ioPr<~twt249ZKg_R?KJoJH?ER0)^b8N@?Xi5N2%U+`5X| zs5NKGdGMG$qj5@Y1!&Bx*wcUmD_-DsK5|rPX&r)c#f;LLXze73G%|l?WmQGp)tGwb zIDvjl>BP!fYqB}QN0m;lF8^_9O?6de`PJ4anN`HQ_9Qgd)n7@!p0kG`bZl7-=BF8! z4&NDMs40uiKDl%<=2X{@N0(RXm^ZExvm8uaF=o=-%BV9=LFt=JsHarcl~#j;l;QOa zT^#(J4~vK`=9SK`r8u>iqoN~2fTRFqcEomnlSz_m=l9a%?? zn^9R?QI1LGYBvtds1ReYTW6xbvTR0aSyfeaIo&kSBh!Jp$~2vVr?S#3k%`U46dNs{ z%^|x79zL4xsWOi-yH z_`YO?dPc>RmN|y`XEX|(j@>cdsA8%(r=|*-YgGj%{qicS_qj*$e8_T7`#Czo^5 zae_xWEvNHvu*-xbzf??s08%>g6mJ|?6!&eNplm8bteS=QpmR5G zR|=~>d1h68omyUS7X2pkhC9tjZa8XB1rbkBf_(eslxwbUt3EfR&7IF_ds?R4VdPjZ zpHQnTH7!++M&#O4k!vPfdbQ-AxScWl?ftiaStf#Gw8EOgJVc^xZH256_bkY`*7erQ z-!9#D2<(A#me1qNmmzf(r6UZCal$}#eYCWCW@&BN+*uW+Gv`F%6W8pS%$Nf(v@4=| zRlTPvD$MH+&2Mi?q!L(CSJu^3V^zx0i3LkJWK<&kpJu!lV9`gidtjn5b(Pl$d*W%x z54;3p4@9Qkk=kIv?M_AT0IbOMCP(!w>(n(~%GJO<)pN zI=d2EWEk(o<=!;zjkIr=UU1a1*#Hjqx+0&;_IBQbr z$PuGQ6!_~3j5>eKShHwej~F!)d!fDXUvK=^N3O#1w-4_0hXD%%%By``EJFIAhOJaf z)ovd^`-ZZC>%jR<+idKy>-u7ppS<@ z=yDA^xUv#=81@ZS>hd3?zHg{bm+KF&#Ex_C&_a~?$$}0}(seA?^xTOZ^8TTpY1q}T z{SN*23*F_Er9M0KD_wr$cR$9Rlzl=^>GC|O4~JTGIXdh%?8Ns8y{gMsNqw)-yD0PH zoVx*%e6EsM!BLL6S^t%J_75W@cPXK;TQqc+N zdWWBvG(0;1?{%lb&kew@)$l<9_|Cf({?GutS(ozz@X0qQ{BZ&JH5y(RfEWH$;YR}S zM>V`S03UI)!oMN_U#iQ?1Mu8o>c+&%0KDS_1z#0_J9JtC@X(10zcm1N@UvH_dPo0G zd4a}f?G=i3`MW3sd~pW2(+}w!gMS=<{H!qWc@NNmJN6;I32(h$@rgkhnuUMPr_F#L z<^b{CZot21z&i~14-L3&z%Mu8-3GkOfcF^inFc&p>yzAGYQP5>@Tmqo&w!t0z@7VC z2m+zm2Hd$nMR=6~ckX`?UTMIc`(K3r$bk3Uqx9JqFm68^8}J?jKE!~B?(_Wry9PYlfFEnXa}D@$27HhKA8Nq!4EQhu zKGc98Z@}{n_z4DloB==4fEOC@;RZZnz(*MHVgr7X0iSNb^9}eF2Anyg^O#-z z*BJ2827G}5A7j858Ss-0_+kS-)_^ZH;HMby*51AeLjUuD3@8}McW zewqPqG2o{g@KytUh5>Ig;Aa}}b^~5$z&i~11Osjx@Usngw*jAMzrr>fG;)Rml*Kn z2K+JuzQTZ)8t|0{{7M79%7B*}@MZ%(!+^IK@CpOI_j0cW_G)0S2KH)TuLkyNV6O)D zYGAJh_G)0S2KH)TuLl1A(7?ZPj`}LnxFtIh55HZS6^b;qM0@ROi!^S?UN7wII(glt zS)r}Fj$MzhoWUXD%Rc(sxk0|Q^8vTa zjq$CWx4LC+h;Qw@-Ys(zW^3nFZkZe4TRY3#GB>`rc3$X~x#7LFbAnsuM)%gvk#3nA z+*>=3cFWw@-r9M9TjqxL*3K-q%#G}=o&VhF$jc3E$nTc9aSi$1GB>Otzgy-;_14ZO z+%h+)A-`MZ#x&%2%iNHL{BD^W(U9LQa|0UkyJg<+f&6Zn8_|&8Epr1J^1EejJVSoB z%nfJA@0PjI4EfzMH<%&+e;xVrr2Kcc%nfD8@0Pid4Ef#iccuJ$nTc9aSZw0GB=DNzgy-;G30m4+#rVhZkZdykl!tH zLm2Y!aOCGkFzoM^xd9CM-7+_RA-`MZhA-^zmbuXj`@3as@IrpK%#B^h@0Pit3;EqL zH*z7rTjmBX}2g*+c%8v)i4+qNk2Fl9<uK0Q!AIZz%RC?6Xr9~meQ43q~1%HcryztaNt50pO*l>ZSZzZWRK z6)68XP<|m$UL7bu6(~O*C_fx1-y0||3zQRqa#Nt(5GdCN%5wtcia`0YK)EDPE((+_ zvpn+CNwL>1kHkKRG;Z!Lo@R|~8Tn!)ae4*Pz+D4J19RKV5jlfzM34`K0KW>5NFw|r zeiigY2O@Ppgx5&j`|aw;8N7&i>-9JACkXz@r}Asq&Pc2~vi7qxBWrtlN3u3XUfUTx z7zF+?RT0SE)iG1%4W~Wlk42|HjOij&e?m#5@$}n(8H&XI5$zv|pS}?VJND(SUG@wl z%NxTDz{|P}ZQcIazH>e*c~OFP6G|pbn;N-(O9YFA8IgFOW0}lFVpjXe76|p!TP$qE z)raILmbAMt3rFGukCEbE2pNXF1PD*MsLdL+MWKE{ zV@2a)m&PwNjRzHtcQ16c(*ely+h9)n7(b0$LF4&0th(&|P_eP81H5M?n>OQ%OS@!~ zjVpTD#-=T}%1$Y=q_vVx#tQcm4deNbD;*|7byVTlVo6o9KLrWkOeVhoSTO z>!2Q!q4Uon(h>OpWn)NuG(i}W3iR^nzXieJ?~ER26kLflRV3N80-QpGQ0K=`awNWt zWtUYMW2?znJU0>(X5xj}v8I<5l0$SgmdlY?(@H;dFc!^#uJS{#SI}m&o1}d-`-rVW z$BJ+1colRTi3^93L~#}>iz%%N3AeK{(NrVyFO!PIfaGf=Bl^idTI!cdeL|rPe9Ego zSgAW-wO49P2)1h9H_vMiHU8=gQH#()@lI7N7JicUVpVi>ezg>rDcYuO&MgGdcoT)s zc{-HybSnzf540N>^~Lic(<)813e(xwW7G5#np={pWdd*S-O3Y7i49Q1Op# z5TH?%NlkWB{{XL~8x;sR=eCv`TXLFuSmQbLf48$3@Wa|R_9l9+6g@3d3N&TC9BW|E zadc9_{ynl!Lq#fJzJt90R5(Hx{3H~*u_Zd&z`Z6N3;TI2d^DI8(y;I@mgNQuAN*mL;15%SNNl}9YbMnjYbTxY81Wv0Ge#c<^PN}t+B&9r zX)6@bH?_%upxHUl*JP!LoVQ8y^m|3=*Grk6r({gXrWR*ia>LVevs{VO4f@X~Wtw~Nuzu2=rsriFZ^ zhVHyfiixJB;^b;R!R8=5S3#2Y7Ju>k7$Yi8TMK#{B?Ss6o0$K=7mx)o1>)64Nr8}Y zF#my*qUvC`Y7nlFr%3)IRZM|YE2comIGFzspi&^V->6ccWYZW_Fyo{}7;3Aea(;F$F>z$oz)@l>(_Mra<73`H#wp zq8e$Vq(ErJ{0EJY#YhUY(Yg^?%wl28x>;p9OM54+J5LE@8qD zYg*uk{#-#9`JwkI=wbzBWjv%Zkfrc$m4JMJdnn+N^qk6kxAFy5Ndhv_w}HJj6jak+ zvsTXo7$JZow|nJ9@+Wg@6t^o-pUaVyNOVj6z|~&;bfvChsn=;nLae5461Y?YLb6L#ujoX#WU{A22QzQ2A9`NWh)i zuT(|VQPQ5Oip3-km}a$U?C=0E9Yn|m!crPc4DvdIQXWQ%ec~{YkX2;*F5D=2p1GHOkg9NeaFbFI z-4m1)RH9!aDwt+XRyBeELO=hEh%$D7uQWU0?Ib!=9P|>Cq*3|ylYJ`R`H6Jski7tk zQMpImP{CWJ8K13O%#ow+Sk(HHjhTp8ow$W*OwA6}i@;3halR_Y7j}5?cHT(nB30jl=s1gjAo4dZ-fx z5+3e$_l8D2LD9I@rSYt;SZ9ld{&S`)T}YuWa8Ym3sCO#VHV^eJfrQrxgwoB>sL!z? z5kB9gah|T2s%YHo(YRHip5dbAY1E+#wai1EDUk35Zg*eERnjg}H1>CCY|<5*KUVhl zXbe%PVHfoYjrxp2-Fk+ri0yWmS5A|h%gr5=!6>*71{k@{G$)$0Y zt~giGX!K|-R;aJJsE2CQ!3uS{hkBVpeZ@uHB6Uf7+eaera1V8qG!B19AmnVVMmLniP6@dH^vrQ=0-gy_kWa-lQRaP{clhIkU zFTO9{%x}xK&r?hs?@|tF|7!gQG1epE+mU!E(%6ED5&xqUOYVA|At&~}{YxUn2W~pe zRmR@|`6Clq`%dV=-J}L3`zESX#kD{3GJaXli|#50TFQ+z?1qYg#v3+T{v!S={hHFZ5nTN*JY#7*pk&XR?6FSxmVZs zP>vK>J@XVo@2-IWJd18Zk?r+8F>6z!wWUwjsnK!{z zfz__~IS*bUC5NF?>y}J%2#N`PRwhL1adrZ+^hQeQ<=K8jP z!@#B=)4mTX)wYR;5O6%)SX|*_=@G6VVx#7LtiYURKcR)l5#XhNM+=VQ_B9Ub%VOTO zfVE*qH3nyU)$R{Z+MQnrJKyyz-vj6O{6czqxWn~}cTj6v4j1AVa;l9R77$xTS*(6D zyz({J3&!8Pwxiefw#JS=F>43-vg0K^k#d9?apO1PvuceLw|2zVzVTHIm^q8B?TZ?= z_nS8qk>EBARXF;y$Iu57;onM;@%$ON!u>7qmh-f={l)bzAFlr48L_s-R@hc%|SD8`9Z90JIIQm;w82x&Phs~A=byS zjTo?PXrs{kbt^;pu(N=@co0kz2$zlyV-sk~bc7b{gTL7eshF#^ zC_H_G;572t&1_{ zGBa_9W}-wfk?S(?OU*?5Z@YFaYS`3o-o5}};cyH71erQ14O5RGg7q>}QZJHmD4dGg#_lXcbd4F7?xc*WP}kwq0Zg(fUbI80;#Kq@ zR6W@pzcGcE9`_Lu`U=gihScugce}q>Rc?;gcd$qD#&_o{-cHYX`Z8hMGznD~+ZZqE zh?j^s6VyaH+im5ODCjMi?PB%YVkO=2l5MfJwH>{))*+;2wO!_o23XU(c<=|nhv9(z*X03$+TVw08UMK3d zq-Bey=3u^(i;FQ!!A)4%0}CQv@N!4o>KSjf)n|)BU)Bokfr#zecX2y=oYm?Ki=aXe z!-NEdZt1mfMI(qeHrTxuCbVi9*(FvpPeMQ5tOZ@;KA(cpZ<)IA`z1_F-WUmDVeJ~q zP}|s&&GrF|Z`|*cJ@2>Ap7e`7MUnU0@*BVpXfaUxwbH+h$v%TVq04Vo%f3`5+>`Tk zE{xrcsdn7DoI6@l$20CUd524SaJVFCb&H3FBaMhB8?Dc=MO4yGB9Sa)=+@`5Q2498 z1XhyhR{s~N#Pp8*^I$or5)Iqmd56y9&dfpYdHFArXuI`p1P0?BG2}2sm}fU(hSZsR z(tfm^sfd%|$OhT_cFEhw0oIfNu}Ftq*|~cK;I5;*Tt>q2RbW^zR`!3F$&_#Qb!R># zE{%2;#%$MoOf_JovSn~n*M*U_cHUY${NvUc9N1Y%d@)!%kW%`pCHdcxh-6_*Ln}5^ z7Q-2(@R67wbDr*#wYV43Op;%xYcbHV3%v7bsm@;9t8vb@UYP02=QrYSzj@=)2m3-g z)dQ;12wT!Z^F`H2X!b-B1D}!d!L+x%IJawmG;M6jS3NDxHP){>(nVq=t%z_U?3T`z z90c*A)<~=sqV|eByZ(!zm)^4Oi=mP2A7IAl6@ey@6h*SIseN!0J%pEp3|kwCy%b6G zIgmL&I#1J{Z9y}}1qE*-;iXBqa9T@U_I)jQC2IQmqhHi+(r2~Yf;0;p`06kDtcqqowSp0(ZiUSLybwhnZ+!$gC9svg*>XK$NbYV%35l#6;_t%mmi5AUN6=%=)?c->W#)g&<4+B~t zk+k3ZoT?~kfj4x8B`buv5K2n$uLu-LO+w0-xDQ~46zSzC9$nM>9|&mFmbV5E+lR8h zl$BDh=!N3|{o4=`yBlsC493a!K)vCO-J<-h;Frn(KSe1^PI?HvNGYE8_7K2D?kVC! zI7^_4!==dJ+8&9$qeWf`peZt{lJ)~6qC{>L@%$nmBc9-ktbutk7l*8r_aazCve6~J z0TC^SS*cZXiV{7Tw?yK_x=}V`(lCUZ9)y;4k>=k#sy-3yMHtzQF&jqWXbP`JI|K_I zznJB^ourNd8~i9$C+)G67)tml(pN_YrQ534#QgpdkOxb{A^h_zSAq$b;#pKYI|xjl zn_ds(Ug>EevLv7mkKR_ACk?%uee~!>gUB3bTcG0`DTXv1gklGaFG&%{gx2RQYYBR> z&Sq%1L@`dd(wTGlWvJ4_F@OPQn2$~}Cp|yeZS8gbTHT$|muq-zJWwTE92thrm2t?Z zlTc&vN|BiENEea7cA50yj!t9rsky*3u0$X;5%fYcgzy8oyqoL8KDyJ+(w_hg6s^fjA%B#YmCaLl6Lo>J(72zvHdyzsLJ(r9S4Gn4=ZtL zA0*Tn5L_)%@0egSmjh11^S5bZWa^BOMAmI=7W<-UyjVPxF(7l?a7Hx)1-(L|(yu{P z6`@0z#_YlLa#td6eCe?TjX^7F>m4RvN`f&=OTIYN9!N=HaH8$Ooe8Nf56NNPatCWdzS1c(K#p}YcE=$42~=3L9j zm=E@#A_0;0%JmA6^>pg2&AfSn$-WnUaLs9NvGrYn4MF?@?-2148DH@94%&(t6f63(bYo=g2 zbG#p@y2(f44*eVLa$BQ>t58+ z2x6HF&|mzC@U)o-^(^{e8fb?5W#|!UAbi(v{ycZ8U|t+!DAqa@?Ta3n6QuDOu_@*g zK@<^h2V&4?Q5(?i*FntS;P;B}r>7smu@ZcQkz^q@8{F$$UK=YOSj#VPs<7MH!Y6G$ z?Q2el+)HTI8QH#i4Fm^B+CA)<3$+B)l3?y3YT?l~<`aQL=na_H*O4(8;KNXpzFi_l z$O;;PY@r8aIkA%Vjn*y%DsE=@b_-7RH_{_9C2GKuK3F;}`NCq1Ar-?+#QSk;S~+1F)sH+df4)}|!Y6G6aA;V$P-oQDa0GmR0w&XnBg6E--;2~Q% zJkpHaVSWa(b{^I*Nlf6czrb0Xbvfp#X@OqxX z^1c;jkJ+1b=1GY7HN#G>GT2rMlJ2-(}y?vs_YiFPuj;x=XwU-_itrw^rP^@7~a9T)|Y(e06BI674Cy_&_=nzz?OK)b8GVSxc`uc7)gV)5=AXKuloQ`0xcyjdwq|D-3@UK zYz0uu^Y7r~f{u0Kb2;*iQ#C9=PXM;@@X+V>!T6q8w_I5!Y4@?=Ve&U2nF z?0;iR{dvxfy(1&>@C=4liBkI>ru_ZNwYblq?i236@I5eOBYWZfcwTNS?4R{K3w9V= zaa~zPPQr(0my3#4&qkTNEXyBT68WD^JJ6)$l<6&_?idNrEpdwJSOc0)L)|M zCk({U@r%?~Vwn^Z)*@`{pC?TuF&W3h$sX?G(iFBy6fJsopl~T@zvBT`F%JWEU)|B? z&jX6{pPVF=qZB#q(Qg)O*&(i*psX10(yUm+mz5B-0e3}(WTI$=+0{f}SN8~QoT67< zy?2P))dz}v3u*lvr>{ty*FWQRJp6mXB)1o75>KFk_pn0e@5u*Ub{U^vh{O}LhW61@ zaS%dy6TJ@71VZdFKJX%shsz1kUV9+=ZCp)Sy`ufpMY#T4sf{mFv}G(!!4&V&eolxb z?T3Wxf&aw=PFj_{4BAhx25ouHB1=)u&RJ53o*?I+qy^=?N|Sm~l;N8HC7Q(Xp~76M zsC=Sq1NB3r^H*SHaa^GOt_g1aSl0Ii;;FcBy{HJ9mOYv;Z7RU+Yp!%H?&Gs*4fms6 z_h}WDllCe|1%3AOWZvIt4<^?=$2Q^D?nl33hFf*ByVwltEM0s_@GPwr6fsWTL`>K<}1T@lB?AdV9WhuEL+sYJBGA{;lw3alLs`>I2ajaUIQ9O_EcT>Coc z9q7_~ok1A%-XV91@OoX`j}q)AKE|G!0Y{H5y8?gz*Rk?^*ibpsMBV*l;ihRm2NRgP zSA1)k6jiJoEKMW`_Wy{SySUd&Q*m}*tZc+_QqS4nJy40g{#iid1CNlR1cIZT_UQMm zXQYjo?;#9a~HC=V}1Qu`3{7s0CQ$JE&t)RG<^Ew{>sbDgO z4A&%@&_F%n4yO-w{t3%Kv|kZlwt$NA#mfEn!2v3H6l}aaKbF(953n0o(_o>T#b2Vm zDrD!}!ZRjudDsj$k0qX|s$L8Gs_@oJmBVfsi}4kTbJ5xGW+%vo7JSE%Xebm}Ak-Ry zH!Dly0*{op3UXrLO?T4+#GE7l2glK*Ix#)$(AosSacAKkWs;JmEbCnxBM`(QDKq*R_!(oHQ)zquZ8@xz6ZyXLiD=taz zMoq6u(R(4swe?GcXj?CYh>C}#U5o2Ru@mMH_JTQN=uUDI{;9MU`)jgILd13To5Z7Z z9-`)u!DqO0#J;~sGl!Inck8#}aJbAN|HcL8kooAGo~-APr#@iO?dy1l|A4Qb_=U*Q zqQ>uy!gA8CqJ;5*FYNEhuVe-uc96v$;RO4YIiyH8D-opd8M=6rvKu(UZh*R8#OpJ7 z?Vm%?BGehiA2&R^oY#=3nnNrY)kRSLu=Od-Ax~id1mN!go)XtzAoAi=&oJxt@48YI(;vEo>i z60@zvUUIw7(oVeS3_cP5!<~+a9vz@Y%9HFVZY@?b4LCo#8~y$0p8^$LRzcr?gpNWO ztHa^u_fx)i2?R|LExBKOKQZ8LAufYH(gE=g!jfc>8W%DI|N48`z3uPN_%*vu6K)^b zs^Q{GS(?$#KR8hAcFT462v?d-{gpJERmGomaiD4=4)->C?<%29@fa&MkzF?Oz{ccQ z2c`M~Y7SWaK{gBK0|@}NdDaDe7l)sYqF{|1YVdHExl-5raq8S=taV}x2X(A1SS7`a zcv`4O#mh^2coNwLH&4M5gsU~lAoq?2p4KqeR9xva)4Z|O(-~-DPZi@Uwrn!~2Hirx zI|IZI2an07D&Wwxo!E=Pib}m}BVL#rU&fkvf;1Fc)84?r+@>F+ob&WDeraqKkr$x< zoHcC`MPI?DL-~!XNvH316Lk$%iHnwzRdI;1P@}?-m`v1g{b3&J4?WcVh$`$NtP&An zbu!0Vyh(&kEUO_;itheE#@biCCj_$Iw~A#mz(j|39qk(0#hV8_ZPqR@Z~p*voDTeV zs@z=&jJ=dw8hYQLS)8v#?K>m}B<`TxGCGcb4|FxwgHN&#^Hyu>4#Yy@sE9X}e63M! z0y_}gos4xawze&5*cOWR!x8FjIMRgs8GKKt-w&}oaqlZ+~W^*aY283HU}o>yn=peh~D}HENS&*p_}o@Uf*S792~~} zE3des=Z+-al!GPn?lRV(Ypb=F7xcb%MZfYk8icIfCrsjfFSLK6sZPi6?NoDOnG_Qi zBH4iVrHR;Z=DR-JciB`g|G@lCJS88Kw-~|BSo=oRT)ap-K=M;I=7h!W2kY+p3XOqR z^rfO?fK&)WYGOO=chUwVkUv9*F@Ns5NuGu_`dXp;y2RJl?}d2MzMItI1GgcG)O~%a z=(mvm*I0yac)HGN^a~R4KBzMB-0z!T$`Mjhv3}XLkLfP)piR2=RgYdR^xjU9W!} zJ?MN5#AO2k@sH$cv9q4@KX_cc&-OaP2GS_$hD3)`4X9axc z_e;s88vk#Di05bbOVc!_TOqqfj+^vIpGmueEXM~HNKxEG^d~yUe)wag9nO;8Xs)%o z*^6w3_rdAnSAu7)prmWm^&(zB$!jwnrCrZELu50Fs>t*VwCaXb<+3wDE8Tkje85xY z)B*y7>-l2{46f&M2sCmohnXLOnUtLCgWmucoWK7Z0E*Fo=8Apb8$XpkRI33jud zM;7pZvXpO3ID%cRd%m7(@|^c|aVy8s86=;TCgSK5{^G-ZnoYeR`8Kxbb`vBYlua+k z`@ia?1pnYs(ik{iisJo+!jST~ZI7@+OYFG-8jJaZVv zt7 zz#l3hTPWn6E74KvqbnN zU3^9GEV1-$>Ut3sauct=aja}6Q57o_Fj*j0o(fv&V&xFPQ{vYE0)w%#=T`s*W95eg zriqm&fSHt7`81J(vGRccP$a(@KrPE!DAYbt^lZk;Z@E6bQms!9jya~-7wZ!0_&}v& zZ+&__?%LZgaWf;X#(6FnNLinbCor%+{SQ|9+FN)wF=c&v%ESKkXgs<4L zYo(B=!B;T%PBe8(Wzt?xit#44!13-}Z0+$1&llp2K!PHQ8=eF5-ui3_8zq}o06`WG zmqZ#@OCf431|vTuOA_&SQYyT)k^~*PQgQ!*e2@nJ`&XhnMNFa3*B~lJaDN5|Gx!=r zkdf@JelaTBgs8ciY(a0&!~7xpU9e3j5NQ=FQ)xJXNkgOnIBsx;XdQ^*S8gKH0-Qu( zvT3Cjva=rExv}YP6widXH3Ub`h3a@kH<*L_g4k|FsbRFIMyFoHy#XjheVX3|Lb@)v zHG64>w&zG&oW_x6n#W}T#xW<~gI*g~^KVvtKgHyCyDAhURDY?0jOd!`T3Thy`TNTz z(P;)4xBVzj`Ae`DMc3nRgu?9G{9Wi(FHskZGIG`|qfUHE-e2skol14Z6Fiu3aC0QG zT#GI?6p!f^0s{|eqIyJAFFy#9jUp;;o%kr#D@rDjoThSaCd8Y>{}aoEo+9*n5aQNC zo5+&18y^r})#gLe9Wr@{~f5%zKL4UQU&)juHVwm5FA@Ao7J(W@}Pm#VeZJe36${mVBm1T z!mSJSW!F{cfNeSYA$qY~E@QPeeL`;f>bJTk?Y~J9IEM9!W$H`6zEli%91;t9B)f9R zL6bLgmDwo0(w?eA?`&pEZlwr z)geQ739Tx?ogVON0+US5zi*MuW>E z&OiLy<*^98GzvnNYM`)lZa#(##hb*3eOrS6xzE2P7{4cFC>C}VctcUCFlm1m0+>Tf zB!&Z?hxU4CwZIYV6O`7ZW8b+TtQpmAn{4DNefGj^!r;{s&T8?WY>HrGF8Ewq(rTgK z;>p}relzM?&1!hzY!e^mP#N$TP>m$iDf`7xZp_+%&3XJ|QY@x@2lE#O6LT1aW{F{~ zR^CnqNyS}+bPB9iH7DMNC*=~;dz%Os^%eHB+8bK{_aD3dF7zBc)!(?K5Ms3kUZTST z!t#6_j(*8GS;WVCsZ@FHrJM0m#V^b5nhb_J2w4dU_#CM9e$E=}eT>dT`0v~37D>C4 zjT7P5b@6Z*h_B;G+H+;MMYW$OiHAIb1$v^Cf67!;e=h^G@7&pR5cI4AJMLZzxyw&R z4rd>DI}GT_uILX`xuffRNP#o4I0CI8T5bw)gyy;r-6}rtvVW0;uy>^^$FJHcprUY_ zlA}YF1@TOPaGdJy6gifHf=`ZoSi{!u`6${nGw1V`tvSbAn{n|XE{Zy~=3KsZYt8{y zk5jd2YtBVh&(@q1@hI{(aJ!>{?2+#o!YJ+!-c8GyXM0c8kVLpmVuF$C3uh~_#I$&;l&a}sVB-UE=u=SW$>O~s2CO=;a0t+tKLHuA77XDo334k z_7}QHt29zGkihfSoZKSFmK*K{1_XuZkW=FRMp^?>CBld3){xZW@_;{@#8+ zye^lnuAYo>lKy6fmH-uX!q1PN@jUX)VR2^Xhvcs%Yz9{+**NWfCh1l{)ihS1=jMM+j&OeL}+ZK^9Y6W_(Zah zC%ZA}^8jbk9)iv~`7i+hDL7KwTaeg#{6z*QC_?Kkd&Q)K5~ zRHrLc>=%{rIbw;6&m$tNA?<$)qzL7bHGsA0NrEpjc2M4C_j-2e8ZTV)4FK{u^h=S( zby+$D#f22akwX}uBwdb%NQ(#IBib#&-MyDe^UsIcHW+Yp&=BWBl@4h!)1@k zYOCX;nFI_{fYw_5j-N^sj<<)QF@stw9~Q>#T3p(Al|gS@V^~;rV@F?H_OKDO)2%gg z353!)7Z{AKZN_{V;(KA#Yp$~7i3=lS=n)J52##1Z2a~;waT$2Z!gOFYp<+sqr>em# zpW2h<_KqE*M&Imq&Civ(4L-gR?Qx<5kG3qr!KZm*`>1rZa}A4)qlb=wbNG5UF=nrk zp#`);Xw&~3M4mg^`8byHf!U-;Z^B^BH(|$!hpmM+hzb)SvcGD-`Cs10ZV^kQJn@AG z8gL6TXp6+a(vRS*>@JqeFDDG(5_c_qPf%r!!E7FUf<4$%I-JOBW{fv;O31yh3OZmc zX@3Q0GfxNj@=w(FM|PL7KIIwN7`-1HXDhkIo8CbWs+hDt`!Yk+W-Ih&6W1XUeA@p# zgU}p*q;@SVy;I$Iew&Z%2X*Dn4jKmt&Xg9E{GNbjoO}34($w=|h%#!_$RqF)z~&48 z@E)dJ&U+*}g>Swhf(f4Bod&IX!A;>!kEWH-;GWzK0+QlRKa_tM zQ*%+L)3HF*YjQfM@a@7deEfZTC@mf6)Nvd~ZD?^c2AY3-dE*e(=YvXyQHt7pGL$Z! z>D&R2tzqv^oWXplifbA{)6eHCP40jII4A4tGA|J3>~pqLv(SD*QhAJOvBFq#Lstnr zM7ZSEjFE;4*sX+({#tWnJB~(EMPRCCIMOPURrN}MW}e^Ste;TtCc&LyFpo#kN>-`4 zQFvcS-u1F+7!wUb3M4g61y0^Rcqg*b1r;h07U^YX0nP@RW_}-7gQ`F!tB|=pB=%v-l z7sz)baaSuJN%5O#CGYFC;zSYz62BYkSCho0Gq4ToWNoDnb4r3n;pHmDn-7e1KAdD4 zMUi_VM!te;S-RjsLbq#p4T_$p1zE}X27xAY?aL^%FB`OjwERP1o+1$7#3|AkLov47 znai8rBGJ|N_5hsvWDJ#81)N8f`QIMU`ER+u$F~Peo`YH_^TwDA|Mp-cy);g^IbS<$ zK;cfd0Pm7KSdC})r(UB16amcwC?zR)uAAnAFnEm+0-ox`)OxNP$7nR5JtUD;Ct~Qs zeRacSs!Jf@RT{=|X%8a>A(?Fp=r zXAOczo-f9ykv;u;u@hWV2xvFJbUvNYNgpD^PFk?o)G z;rX>aeIskXJd-cWlSw_8yj=eoy&*L;y|y?I>UP)vH1=QFQg~Jl+&d*d`s~y6UCa%3 z!5}1#o+@SsUGf}RRDoA{j2F+BVq{UfcuUMu>dc&|4-n#>hJCZ7^6(qPM0g{C?1LPy zw2t5C6r0lV;W^xG+*v83R`#mwM?g2d=OQk`-MAe0URi)&arM2;Hg$|-vb@K6+BUn1 za*UYuoOXM4XPXwXj%`aqB4E_ z;0ei;xfhs$*;Mf*#FKUoH4`_&oy)jJ;8f-X!*5_w{Cq@ai>AIQg{Kevo)(l4QgYnM zO!j!A&oo*UX7=FdU9->QJSpr;MA0u{=>T<j&K6?<1gnV4B%+YTVNKvZOM6y?nG#nFH@IW%p`JQ^r?> z*<+)Hd%u)D5m7@(Y$w6%EWNB_d!NC5HXH^jnQAwx`x;|#uVrI;7uK3VATo)hlS*}( z7-+rD;U#mo`byg4p56UU)zcIlP1fMlH+&~)KLaiLM~F|P=ip+9px*4@*uYtxfdqep zFAysL2@F$?I~f`1y@MUMz=81AzYEoCdG!19-z8U~c4RallC*Hg5QiE5fIs}{3fW~r za~V(z!Mb?t)uauz;h@$u?F|R=@tIa8&0g%;LFTiWUY`OG?HlQz}K_bZUMgd@b5UX6qrIR zMNtW{uN*6s-$O(F3L%Z1jq3!xMHl~$V&{vb=AjfdkPG=Ox=3P~{Gwo@X}L5I13mR! zANo;(&R=$Q`q(58`OTOeH~|A%i`Y7`OnsR@zgJ0G@nu3io{*m$bLfb4*Z)M=btr!T zcCkm^i>E)F-+gZJIQwv+bR*b~N`&R(Nd}W-<0pe6@dM?42wvs~Z0;9j!Hys&bkvXS zX-_;Dlw<3$=hLM6NZM0iPeahNRk1@jNZJRZ7c7pDupouhFB89@E5eDG*}X?*KR=lr zXlEYNaOiEg2ED1()?pCbzOjK)nCrUW#5?gj{&CH9$ZQPJWc0Ck(JGJ8xp$M%2NyLws2F`P!07P?qcd>rF?tS49-~EI4vez7kI^3i ztI=8o_Erg_kKyVumr(|@P$y2qyBdvKe_&`!^UD?dy#%}m2i4!`Cm1&1Cr*X>&+!*O z_0H?A>*6IF;?{Hc>5QTl9DvEgb!hmji5dt*a0N>@nf4=DWsdfA4gRRW}cNQCV_ z(y_&n%^4_i6mtFMNxrbhmvl4g<5Arr8t1 zrxV8$pgsB7kItXsT7L0E!oPCl5x9w~e#Ivg?T?vaJDxzyS<|9Vc*-Jfq+I|B z@OHAk*C7yvcU(>&*LVl&>cg*=>RwU&^34u;Dm47Ii}Y!mqVdH7s!sigNY0XKs4fx@ z=Sd}LZPaK-39TNX)f28i0aZ&~S`TQXM?eclZ$5aHq(x;P3-unDq6el3gZffaH_nbw zJJP5&C=lOC`weu(>opfbI|K}Yn9i?$oQ|5dc*zz;ehu!DF1ETBHFPOmb@_A!gSYcu zi?#EliF03*H#=i}31WrUBF(O@OE9=}4O@(9os1!Nz^ zTl{X{FLQ0LiD&e>6Zw+{l8wNjgvyp9<#ssciS*9L*Zqm}rRGMyj0;=0>duhc_t(Yd zM`;6pY?i0}<0TKdznQZVclL$d<(l2o6}!6&*T>)|LnI!+zL(?L8~lYRNfbm33w0ig zoFrYio&hW;T<0vADx+wk-u$(CF@b5%&#lMQ=cvF(A|Nr|)PVMMO-nkUlIG(~Fj^8!H&mr-L zjZ|=N%WCx-_)>5$3d$Va&pzB!Py?<#1(l*C(VYhFN*mp80*V*iXNq;5=uYi4T26HD zU~~r^E)tA{Cv3lmN2dbO-Su@uo27wV1Oo`(o3z^xp+?kn{{qRnzOQ`e9>6f;yS}r^ z@tvjG_a}gLQO$1_HT)(Nor#I>H-U+7=wY5}uEe#cnu#buH3;Xtv03z(;!1MRFG06! z5L}7Wd{f~?XcV_r;Wq(e)+$~w)J}(%;?`pVe2f;l=(3J@H=%R*4 zm2i&+ggbALCtMS*J>h1dq=kD7!b!@cldmHj;YiIV+_h-*ZG=;!X7vB7qlPzxoKf?* z_=AiZWK)kh1BRB^otJzEVu>OjS=8`|Qsg56MXFz}Rw?Xhhnk8!3Z)EQ@&@RNmynuI zk)zSb8&Z>eLyCUk4XO1QQsxvX<6;yD>nZXeJS@!l#@wN&Ls^|C}x5K7NZHLPKzW2PxJ-jDi+)gw`?%CMg?VaSK4wu7oS%+P6>MStDFD z`}Y*Sj)TimIru0&2bPb6kExOX2eVQ+m{7v!_^#h#Gn&L!4}z|-y3UoaW!d;LBT+j~ zbT=uSr2U%%*+W8b6V?iz&jSew2TLiwym6|jP)Lc2j>sicLz;%N0_~`-qxcw2e3=3@ zflDwUO@7Tj67r_22#))E6~9%q6a?=uz}aYzK(U86QxOon#4@AeSw%oWlJ*rK(0MTj zrtS?~^`KP*Po|I>o;pH?{r zm!U#GLnHkR4M9=&+DEoB`0LGKY-r#WDWH2l9#DcKje~IZXqe149~+?K2@E6x0nL06@hO zfZQ!q53^KakPq`NfE1=I3(fGYUY`6uO;dwl4dux}iaK0#;pdw&PXl`t%G^mK!ExZS z9bBe8s3A*a;rCS1SLfPjB(xCl#Yam}>PPREg$^%(*P(Ed_SHb>`XPkpeg9W=tD`_n z?bnWU@SXx5S3T+>SDq~cy@ z;+j@EJQa7UiECQvGwh;#jg@8&6RUi4D}A&hot186Pk#ShZ6dl>x+E3x4inL}(kMX; zSSjOUdvB~p{|GOU83+uC@R!f9h;%#rtW@x^y|)CGv`@ZL0+cgC*X;JOJ$<7XBOTOm zDghVxk>1-{d}jna-@y;qU_i8o>h-;#yWsQ9_{TJU0d{(5$PnLBzVcXeGXS``H1cKC6N9b63gP}d0lM2kA#JKk!qy-RxT8UzZ+0(?KG za{%sOAV@npfdA()N|~94IxuH%w6-HalpwTAQ0l{@h~&XcY~Lu`?OpgI*v7wN&UGdc zJQ;?baX%e`h-kQevs9S#?H~_D15EmITp3)gj9ZpRinHInSo-Y zNWKhDX8&i1gP9&&ECEIPOVvgvQ7%Ug_wWYuG;Ze84zwR=eh*eUEel)hnY00%LoPRS z{@qvxZ&4fyKRJXl@%psh(S4n3zSdEN&t+7s9Pi>mA>7eH83y$K5cGi%)8`GQeX`l? zaGd+hXhYPukZd}sWB}nr7BqnI7YaSgKcsjRoW}2hs#7)M(154F#OGedJqMNX98EH) zfh1D%q+8>Aqe>c4gsLye#&IH^AjrtGucLonx*KMFWPe9}gUfVuwHG{e$!>_DTkifb zn)1^45iscNQJ1(oa<$JjcAV)8NuB}G>{T?AJJGSVkvBfTiAzI4dcnfpG7~uFYpmZ6 zJEXO~mN?^c!@B-l+&0=>`<=UsAP_Jkeb31xVWrx@n1c4NrOkUtqr8oG0_m6iCEaL_I% z_hTaNn#FKdbu_e_lr#S3n!}Cb1+c}bAn2YdP4}xe2s(&X>ij5_?mLj1BaQuI8!VY; zNIiehHj{@@K&UXAu5d9@oygJc>S;W`^>w6|{$u#fq%CgeiGlZ?Nr#dxmbaio-0|^h z6}XRIQGUtIka@WvdNWyO%rO}-^_U%{l|ix2!S(LO@ATc(JI^TKJUGsR%Wp-?6I|jq za0s}Ml5vR9J37&dQHMgNomN-#Wu&pSy~ady|44yo0^kga945As_f zMam$NwEId&7>t%ZIrew?0dVZwDM18bId)C)nz%gw`^-ImbXL@QaKB1)6)i&jVLj!H zo<0M9?T%Hg-#7J4w6Sp5ZY}?uQm;qX@i$E;W90sZ_197ijk^@ryYs>acUSLvVTjxG z&4=A{%i1po-E#|nr`+`;8qIOW3Zvs=zx4S~P*+i+X7a^KgAPxEnema~jy;O=nVBZ7 zIvYN^62WQ(gGK5unO(LL%iZVcIdA4--~+%TIRa@SU_1G^O(c^@)<_(XK^% z#vr0=GWV4TnUzk)K4PjagT#Z=uu_rN_R9f4)$bfOP`UJ3V1YqgO)f5pKm7~M+*et~ zEr^q}|3bQ|*?x+1BV!NSvxE)*^FWSIIdL8*AjMGXD|H@9?(3U})R^bQg@MFiYH#w_YF|l{!#P@kZeI~otEi|FpP5P(8OP^mDZMlgLvm`adIEgC<8KkDJs6D* znKE;&b?|7A+7;sV^c7|cF{>?Ut%6jzZxBB1QLeMqiR|I961>}L$F2Ps&L<*V^QBtN za*>O3F7gaXJAPHvYVJBhBXjuVS2E{lY=Lvp zzv#2OUYCo;mj37wNBg)KB16A<#Lp1i%j(9fy7?M$)R=EHd8-$^5GSn&z5gF&b zf&6m3P$;C|J9wNBn<*Ok zR|Ic|GX?RRHU81WS8DVn!;U)ClP7pLMq8TEsr|eSv%Sx5TEm$Zga4P$2A5~(+sCcn zCjJflX^Maw$5On*XAPNqVnuDxn9M&fV6Z6VN=7Bmc*GE+oJcI`(-G_<#POrc$!mOm{sD*d8tflPiPVC4l zZGk=fPO20c(c2=4{ycI;L9q=shWmgkb@CY{R?_Ow)c_~${WT$dN?@c6`g~}JUAE|k zn-$L;*jVhyRs^v>hwWyvpj&Pi<)T~eY1X&uma-sKd_xns^BkX&8#w{TZPtF(8!H=h zAhG6r(h_B%Ul~Qj?ue*|lA9xmgFTrfLV|ZZ_J<~3R*-We&x^~wWtC(g+dda$`Os87 z0tZ!-hKwxyCxiK}A&z8Uad{SlHf6IE3<;0_CRFi=`J>1I-wv?7yvWUL>?6)+Yg8xg z3vp>bH<|5xAwk_|z^Q=1`xMk-;=7_)gk#+LaJ}nIY)!xcx&iDa=@FuCzaLLQPhT^u z=IQ8cLy{h-*w%h_0mvCSC&$1bCvJjyqQe?E{~)7Ob{FuXW@qcdjM}-J&{>M$U_7TTgAf#x_77oNzq{kNtFEMb`#p;c@EvF>D?$>oiQx%J z$oC%bk`V5n+H|J4wMoCiGmeC#!^u7Hypcjc;sJ!s@siB|^mJ$CE?sKwayXX8w(~Hb zR>cSO4G*@Y1b1O`&)5wOco>rp|JoXFrdBR@q6`PdHsI*@r72}i2T28nQWM486oXh; zOe21&w(9N5ZZ=-Q22i=+VY(KTuW+b<$+{F`PUeRxD2*DzNI;KlzKvJ;HDRjNoe_%_-e%rF3DDdXF2e>^bO)9v4=TtnjWMcX;_ z=oW2s&2v$7mv9@^`=*O=U&33sT|xTpwu&#qan@CmLQ}iu2>Aw4L54TqFr*lg;w3B& zThHwsPQv08xX05UrDigG^+b9AhB>ZJ@;A+sBBy>#vfgo>mQUCs6v=}8l0`_{f6?^| ztd80P9cq8nka_z%kVgRN>Vr}TrwcP}`p_axzrSV5#b8@650m##rOCr?13_l(w_@0&4Z0{llG1t}fVveDRx zX`a)&9^%A@RG00Iw#JN$F7weI=JDU$bY55@?u9p#bo7pFZN?as}5WsaeKKtV^Cc&RCZ$CFQSQmt2IKYe2)A#J4V4qQGt7Z6$bX*1qhrPr9_nF^qA|upjvpds4aqReD%IF@mjUhg4IbcA;nurqB5RC3NA_}iE`bg#YT1( z&j(TV*P@peS1T4|QNpPoI>F02{4H;mmT2r-l$`0bR5C#>y(oFc*zbJ}rbSo6qe!KY zA#JX7<}OdDlXQ}1TBjg!d!&%=9*k5n{!N2AV;^$y%n3vDTd-SzUHX*$1O(uX z&kw`AY8OqXmyV9ae*(4SM`8;Q`RibSpA3q`YM_w?gCg;%c}Tbs`KJzDT$~d5#jiwZ zjB8@LJG7Fs#{tU<4jV6KMxzJae)_n3iir&)hcAFjqyGVK~1~a-AEd=KaIWCB7 zDn}&Mnbslb8D&fqP^=uNm0hn>u2N1P5PZK=DYzRm9LWu_88_XI40uYsQcHi{-A(hJ zq{CergIPN-Y+pDI`(^!4Ay;6Qxh`D#WdIHHf6dZv681aXwH$6Qps3=S4386}m<0JY z>;tIvojsI5=a#vlU+CSN7e7!f?HYU=CU!8~86@?-u}?pWW`>C*<@H&N6+KAxx@Cc! zVIond)Lp)H3v)u%Od5=&LIS?JJ*}7?1$yr|aM#ZD3b@0-Dr!+77pJyQ#T9CBr%|-# z?!WN$QELCCg}Jjj0>!7=u(8~p&39K}?4sp(tF4|l-)t2=UD-11n>LRd-1x7N;|9ZB z*hF>RB7LR!{MqtE7BfQMEr3Jebl zW9ai3oPpULs>OLMV0`ld@DW13F#!I`89=r=WB{~EF)#pLAkY~A-nb9A#7>;GN%j2Q zY69{k?M>1pS4vDh!G$DE?Syia17juQ1N=d1B{K2NtA@zIB=v1H-W@C7&^uQNXRdcX zAVuyD8s2#yf$+{@^iFl`E|Yhr8&pBx5FK6N_-*abgN3=zSp4quFNOq8G^IFp|#cXJt=X0nv$@*y2=If8sK4uS2=f9US)Jds2FZl zmuJn5R@CN2tLcu>%DMFwBSwq}ojf`}|CD@u#^&dbEI6501>;niUx4P19+RIxE+2R! zi8xB2gy)YE#4#fa^7Ex3il-EWCR9~bmzSY~S>pHAV63jDtXz7VQZaW%MXj(k7q%;_ z(yWt{snN1pX$20+yfm1qrKpQm)Z~Tos%Pefiiw7nz^NDWmsiiJsj7%p;Ptb`B^O@| z4ML4SuqGZ&ze~X$DoS+P=+O`g?Mp*Uu_jC&K5bHwl~+cL}u z%?#=}I=jNrKfSZMKAKlHV+KX6s+?O7J)llvNFn zR?ew#==s7si>_u&f}?0F{B(`X&arQ{i&go zPRbiOBJb?l3UVEAolr;znmUP5Lx-fg1xz$#*mzPI;511-UiO9s_gsU^9D$Wh$N(FqmrQ_&Y3p` zLPeMn1_qcIATsnY%p91}d241sG|~}CiO|T%NJ+`C&`8nL$Qu<6jf@J-6pf0EiU^gw zt;n04|Myz!TIcMuXYb)9@8`My&*wfL@psPeTJP&x*Sglr-qU`D=i(1%+>16Pszil` z+5!{~&$U&<6j&{4YbQu5l8r5)rMyMMb7$DT@{N<$#Q8Rqn&sg#j`N1)@?Vj}OBNuR zR&Fk7*@W82IJc#N5*Zznsv#>Lo;$TgG_ty3RPGFVqPnTRk%55m_oZja#5gRv*qmWv zvxZsQo|doZnreS#Yx8^=!9&kvjI=LklYTJzYV5(+sAHfnT3VYI*4HN5 z==7R~M0Jy>L+-rhRwf&#d)hFzuITO=URi5vbL*&FZ*Fy4eNDWrT?Xx_QSr%fGnch4 zsAJnMRb$nxt-W^CsD+6dX2jfv>el&Uf9fbfWA&nVTm7|k zs7C7on3ou~rXa5n+fs~)D#;re%ZtTg`LTjnVXP=t92*fUiH*$9%a7&f=NIG`<`?A` z=a0xQ$sbveR}d@6FDNJ|EGQ}{E*MczQZTYGuP|1aUszCBSXfk8TsWezq!4RY6f4Rv zDkv%}Dk>^28c|eIG_p9aI98lrTu@wCTvS|KJfgUyc;tw@5wQ{ZBML?ojwl*YJYvL% zk`dU*l2}Q8NkK_rNl{60$%vAYl93}R;z+U|Nzx;UHWI2!#gzDet$wkhap*b5ep<>V zNLBx*_($pG_@C#eSZ~tvPW_a3i8t08?|sahz>oY%^1q3veoB5^oL|&np$=&C)Ninp zp64dLnbtWnE`e9>d?owy)K4uS=7>iAm)Gd2d6CIC*IS(Hoo_zpSvX(On=*CE)bZ0U znVhTnOlfYy991{wHZ)h)CThK0Vjl11e#*PhyU4pd*ZYXk@MEm0U zF@p<$gB7oDYD=`Xqr@4fW_@fJ?=j4#AQMjP?3}~*gD3M|da&2P&d#;q`oSEufL}VT zvvVU4S>E!|&d$-ij&j)rot<^yumZw^Rh&0&|04&ud% zA1Uta90NWC&H*pwn6v}Dh%@>1;B(+MaLLHd&i!B}R|op=p4RukVcIwu2kNx4|9Y9B$7Y06V~bys_K6 zq_eXG+z-wIPZ`(Qxd_bSitQS(65I@43+@8ffrr34ZZ;3$#mLLRQgA(33to0#)NDx<+E;B4^A;9_taxCXq5 z$5Xa|tHIsibD(!BcgVn8@Lg~WIGE?8=71N09pG$mE!YZf0Xx9m;2ogH8_srsxnOS& z5aoUSx!@ddIM@MR4z30FfLp;+d6I7rSP5pIMn8Z>0m9G4=x3p!FAv*;8t)mxCeX*%w~Lzoko0c4Oj|3 z4%UL(!KL5;9v)u@o(=8+OTlc$V>>tmyay}=Uj*~cq93Nyf8bqU3-}(m61-^!{SQ74 z?f~Be4}fRRr2mI_-b^qLydSItMJa8-6>ukmaI21g$j(FfYuns)Ck#d61fE&SSt>_upr=9$`tn4i! z9(dVjC?B{KYyoF4?(AFzp846%&PTv~;7efn^^C6q&$}Bu1a1PC(N52U>%raNHZY@u z%N1Zhuoo-Bd~hf@4V(bBf_30+;4*MCxE}l?xD9+@3GE2(0Q<3LwV2mM<$mEd}? z1JR8(azQ0v`bnfiHoB&+)u>z|mmq&FB|+8@Lqw6SyAiy^Q*RL&3dZ1=#Cc^cNfo z-U&_s_k(p{@6Xd7;81Wqcpcn-J=ybnAC z_PLFAzrgdZ1V@9#0M_}7lWezXN>(p9GhI@2+9|fTw(&{sOn&%{T!Et!12)cwXT*7$@K^ za5gyV9_9gX6}T3B4BP_Vb1!ytB=+?_^Z~pbTmcTcA3Yi6d9Qz$eg?e<=x6Yo50d}K z=nrtk#h&*MaKjk-Ya`_VFL?;PEA_lv!G2}5-^17=@DA`L@KNv`@Q+~orJi@%_ps;X z_*-E1M9&)!4gnW|rQjnX2QPb+d?r(l?^B;?*j;cj*bL?tQf_bzc-bcAJ#anP0d51= zf(OAZ;PM|ZuEDinuOj*Z916Y+P5|El>%h9lsVBGsTn}ylw}HO~_ksogLHuI$3mghA z2Pc4=z&h|1a2YsYGx5Q5z-{1ka4&czxQF&%4rbHd_k%;g=fMr&K5#qO`w7|;JQwUk z`(FtT16P4tz}?_(aKe+ML%-Yu4h46B6Tpn8C=Zwit_QCGw}Gp`z2G{q*J#E!I21g0 z3-Q3|U>$fJxD31>Tn|1EZUYBDO}W5Yu-}E~+cW4J*Z|G~7lVtyb>M1nE4T^V1MUQ~ ze~5hn^T0tD;fH`F;CgU2cmV7GdvB%xz(LQFFE|U_3+@K{pnvay!@$AMp|9X@umP+E zH-JmP?cnX;e(({n4|@6{I1JnaR)Cp5A|6<{4ZQ}JgYSTAzyX(_uV5iK?8nSI;Dg{I za67mf+z)O7Pk)~Jfy2RrU>!JUEdCW(0b*jU@ce*t^sSo&EQh-O>iCf9=H`O`8n~yiC{M4a1l5Jyd5kB2k)R?!R6pGa4onV zd=A_OHoZVS8J{b_>EQifJGc#81?~q2Fm8vvNI0+(oDQx8SAq9}kAR!Nm%tx^?|}aV z2aLzg|AKhna&Q*70bB&Gd5Lxa_kx?jVZX$Vf;HeF@D6bB1j2)(!I#0=;Fy=O1K><> z4Y(290v-T&gPovc8QH@Fpi8QcTz1G6jW=Uv1HM}wu{{a_uq z9b5)({xy0G_WBKa49*1igSUWvu!md0Vc^SPh1kd6qQ~Gua0U1Ua65P(xF7rx*k_98 z_1H~3um!9Dmw*l69pDPE6WjpK`yF;2Tmc>c9|!wQrJuk&@buU5@4y(?0*(V$f-}I4 zU>mpt{2X`yd>0&0i9Y`xJpdPi)4^N8cF@~{9)We>Ch#V3C%76s2>vHH=;NMu;vX1K z;91}-a5cCXydPWx?gcl4J^qLufrG$9U>i7?dE+KX_Pez2pou4Z;Obr--o>YkI=5iRnXo3^ z*LcZre}5;w{H^Znt1(b6kFp!HvNAGELXyt_FoSUntX+kOCA#MN#y?kv(HM;C-cv#@-8NwThH$7yg4nM>r8-L zxHZWCj(oFUPFXV_NTsnw()cLj*dHeIY7;=}vK#rE$PcB-|1+6~T!VOiXlLice!1ka zE|td+!pu6Sv-2!}n9EY~QslFdGsJxDRhleAE%N2a8`I>qN%>ObJG+suLw@$TjN!ES zzfZ>Biu`Tl{r&RtRQdKG--F!fvFr28Q}XOAY!dR*{qbj{hZQRO3hG zUyFQKH~B9`{(3k0uS34OoBX$G{HXl*Ab+Ww{Ij!JOCpboFK_hS-c9*Rkw4#!ycYSk zi1dfjrt+7!U+smC9>;j;$>?@{RB=nzitHP6vi@|OX>8HwjihyJH+k(qzM>oX0p!ag zNG?T!OEr1i%I_hzs!QvH@>n)q{evz z>1@x#|L~`CX)2!;$TxQ*-++7r^009({jgo-=lavHw&~}>Nj)AT{#?FipX4iNrk%_r zjefn^2OvyTU*#d+hrEoi{(6eXA@M7bpAZXgt6bzQ$cG?*g75zLnGdGgVHIJXBurF) zJ%W5A^5On85~=tvY5X(%@>wbQJIJ?mlm37Yus4D{YQ8T-{zy0S>Bt{L9;MIi8b8X< zU4{I9;`jBJzbsY$N06^Zeu`iI@s#`}=-elfpX5(}Vyb*4$RF-TJ`4G` zkmGHn`ma3|e-ZK@B99s)tC2sAJWPkAew&c*Kt9Ny{?t_ZJCScg-sG2OS|eflANk1z zot>AZ$?YH#`5=sRCGx29l^~yjJW79NAs>T$fG@$ZsCd**Vi6 zKhrLTP5qINB7W4IwG(*>@(cX&bqt#NBcFi$e81f1Qw(C!J~JYIE^$hbUr6~rV%i#-rP4G4fN8N6j^BkoQ9#Wyd!oKM^@qN$HqxKHY`f>n8po>*V%CFc;I*-kXp0D;G--LXF zznwC_X*x^#DH|m&t&Z-8A;?D|kD3okk>??wotA!8GW}ZQ4c*9>B43X@YQ9~E{C?yg z^{0P@)io*qR^$&Oukp(>?FNO&_aOfpa#4q58(R0)=xH`a?hbsKgkPR%7hn>92=d+C z$V-u*&3qP5i*FZT62BJtyx@G7Y4>?Vz7+Y-kQeylUzw`kI^?f+Bj1Yr@7>7vAn%PW zj7mS7O}W#NPxt4aImr}2^l&!uWv*Gx_w2W@C_X*RTyD!L;iMkV5bjSB?xjGuS5x(A zC%=X0V;

Y0Xc`S0SH^?TMN@9zouL{7QfPOgp(tIr<=f9{JL2$`SXKV*=~$wrrD; zw7cBtIlF{C)3kKHY~+&8bmaFS-@^Ji`}#mS59TDpEhgNki#j_Wl5zU1FWmSQSvO{9 zeM3g70EEqk^J@$u62_A^HkZbzHqjk@0S*maDB)xdou4@ zW?v?|*%waA^}5-fvHRGB+fj)RAmOeGge$kCYJjQC91eSKkkdnIyzZ3AJV>LK^qHX@hwl6ib0RZh9@Ci1Xyitb1~ zM1CFLvv0*a^4mJ!8mR=wr^glH`Q#FJmda=FJuMC2yt9}vBS<4^-dlrw6!It^bTjf2 zFqcKy*g41tAdl*!4&;N7NAiCke7Co{%+(GkcYLg^r1`?7a_0kr=K}4*?zgmuS8y%CQt1RjzNA0@~HC7LH;=M z$oeCH5_wel)*}BNa*(Fc-$Z_bU+!BYcr2iPgM6f4?i=U1 z$PXgNg-qpN?(2W#`;cR;QgYv3)Etc;mVYi$I*|W~_&59Gi;nAFh_u@->M*g|`ls1) zknX)PmMsXwjHq{=FP-+2S_sJirFAm4>NY&}8|ykW@2PjB$2pJ@$-$SaV) zgFMV0X0=OMoWdDObO5_tpis5WlV_-FXj_w7xtL_VjR_#2T= zL(cYvPY+UkyaV|d z6M3=6$LXrSIFfI)HpM@~HOjhu}iws7Wfm z?<_bE`Nxn)wNEAT$;hMZK@0NP$fL@?5_vuHsPb>r_)+ECf&8*=(m#NFKJuve{cuJS z$fL$j9`g0br}*0^({4`7_^3qwGIHFgRQvh%Q^3O&- z9{Gj-^83yJ79*Eu8>0Ge4f0PSk6LqX*7z*rQ|)KB$URek} z;`h(+rJr?Na?L#&`6!e^VDJ>low* z>$nfg_v`__vdWmd!|Gfvvb>?J+3ExSlZ5fvczHJEVZvNS_*7X^XDjl|%6{ZbMJc)Z zK)F!zEX$k7{}Qb4uT%Av=UlGC21WJl&cnzLb|aVes6;-qp|kVTS+qxgEP(4fY4@6} zWZ&^8ZN0V4ot-xjCmAMlQBpn!d1gzryaV~k-PB|4VdPs5Bj2s^G5+$|>w#vBcntnx z@(;5CGy(Vi*q5PxKbCell?8-r2X{B?kyKk$kjUINwU=%@*sBF*zil09%|ZT6bTIn^ zzO|Y+n-a5eOqY=DC#V5#$Gv*ZJ!tI;gf=!t5l>XzG=wgA&Gf@8l3+ zS_u>7s}08RbRdtizoU_V33=E&CSz_k@{`)NZC!i#)`p8U{V4mo2KlX|U*xZ+-aC=> zHzQw%JZx=g>>u(~$Vd6((*}}+$PXcZ0C|)zHh38OzsTvfRKNS~VvR<=ryKcf??>J*BL7_SFIBm$eX~E02bpYdYuqJF9brmI z!?bt3FN|+bW;z3^jxb?9sMuDi$6Vyciyb(}mxheb&Xj(vAsu<9aSU;iWl`Ie3$

`Ic%Sb^o{M}N@+cp24D!d3M~%@r$VX!TF*PY4!DjWCIQitUKsiNkzeYUe>PQyp~!b2ztJz3GWec9s36P*%cAdOG$0>_JaYW2 zJl~&(Z!dEL@{5Rnf?w|2%iNB9IP#Hxxoj57^U~6> z2UgObJCP6mLUdgZBJYdbj7yhCbxNNG6|gUlJZ+zhy12QZ1bH6vsJ@(~a}^G3klPm{pf`O&tDO3=Y}HB>qb5S`S5P!b;ySy7adRPwC}8L8SC4uxjK?~S|3QELK3{wp@;{S)ls>OVz6*KOc-e;hC&;75+g{|q zMjkfab173V3ceS4nmw_;glT1|!!W{}yCQnsT7g{p_$_}OMPGlH8ka4EIY^iae;A$Y zB#)KIhuqTHInFO94Vh*{F3-JBKn|q((YJpu&%?)&Q{9x@x4(V>`9kDT?c9%p&^qK{ z?Iii(*#p zcnSGGh##d3?;t;p_)&EnfYBX*JgSWfk@rA;p}!8keXZ%phaeA|N-2W*6Y{hDa^HQKS;(dQ zQES0P$e%?XH9xOL{tM($<9HMDeaNHAzZ1En@y z(R#cV`S*z*HIB9*f3zF(WCvyf6-0)eJphI=)L%XPrq@Zckl5zYx{do9-p(UzxT)E z2M~UJ-yGg6`E=i$P5r%6alCg=&zuKO@Sf_K^QXSvuY2bFys!7Ko;ly|D`mR7ulGc+oSWe(-|^nRDBtnk z*N@A2>Ui(5<8r=zy!V&hlZd;rPtI3Q@b2rAgNO4%A4%=^C*;s(op+v=@xkMpmt|yp zC4=AJ%9z2Q3tf}R*9>p=X{I=R&N+p5jO5Jr7Qj-xz6JdL)bKB4c`r(pP+lDgQpOJTvDDS>C!Vf|=CHbFT81 zYHBA;!g2a)M$UIKysb*rR-Nj3ly*f=Z*xY@uX=eeW(@ppPw$JF^8MSHIj{Bfp2!^d zY)`KvD-Y)BtbuR!^lr`;>DRLd^6>9o^L=^F)UWmO{wpWvxnACvdJKH1m$$9Q0KTv9 z`2wv^xvq?p&o?r1HXi3~%gFia2fW{841D1@@6Js5{=Lkc_m1;^oH_8%$9XHV@?h3w z=WOZi9mt+_dvEU#IT!Q&(H;Z8)7x9oQ^G&cbLua9dw2KBS@Hqz(Ov`J=n340lOz-QN1NgoxlO+U2I^jav=j*T>@}4wG&Z|^4W8imsdiQ3^_a`%RcJ}mM${hH^p59lpPKEhl*1$J@o1cUfy#(2Jrn`u4!s7qp)i-a#r{BUd_n)Q!nqW zjDbJs>21iA@6TrD{IRF^tIUBf^z`n^Iu+*mtbx4W;D+o`eBYiuaAhy=dpQyk<-WI< zcXN-N7wMWF1E1*Sz0_j>-yd=LxJKEQ>HQ@mr!&*LAv0%RruVJP0et_1HcD|w=0 z6_>V_e|^qF)vIzY^V)2FT*;df|{P7(b-q*Bm`)tj4Hv7>Y-p{1Wyg!#^IT5W4>E_}*(2-O zteibLS$AY#bYqXKH?wnK4))0D?BU%ht-y%>Eb|qHl8W`shxttFyP4jXW!PzTgLrat z&RrSa7cz2wnIU74zlr+&jL}-pi!bxM-(}?dBrD^;L=*4MK4VL^_o9>>eg5Ci|F*#Y zTP-lhEEsvWCDWRGu71Gc@IPA2Rk{7e8Ppwi7_9k`Wu8~e{Hp!ESn<6(T?~txG+xCL z`}BDum{4r`iUD6wt&bwF_46u2)-xmdbSLp9P zHW&xr_RI15d))_ZOyj@7ZTXu0&HQ3Mxr*3f^VzL(@%`k}%Yyf}_d1*9>qcBC`Cy04 zXJ}uGJ@oehmG{=)`?P!?(BEdg!FRKMR!%^pdAsE$S$s0h|Jh&E0%$T`Pvr+Hdgs_L zj^AxIyi?`Q&9?I6G`!2-_~US{e*e{J>qqW}dQX2JILk)t)Zeb02lRJ;mH$(JyYw}? z_YHPL=wIF#o5dK#O2s*fEs7nAD;3u&ZdBZ&xI=Nb;sHhe3WoXgQ_NM&QyimMsW?Zm zMX^J1rQ%w}jfz_ocPQ>wJfO(m;IR2C<|@kiMLuH`WxXPwIf^Zc9f~Uz*D7vQ+@iQc zakt_DMQ@x%vH=&9HUsNI7hKXu|sjC;#$Rxidz(I zF30}=UTZVDjXZwbxKX)7XUtvD)V?4$78_nTJn#J20`oI=U4H)Xyu#u6=cu5&gd~@2 z@2{P~Fk)vikgUkEl3WcSdre{C#SUe9{k)BnG@wGR^1B0kf%3umupB;v)0Q1_8P9w- z;LR}K2YSw~TbAei_#*kbdDrpFO!$+N^MvES&6lw9Z!zHqdoKj?{W-j(e_jn=_{_B6 zNrMT0gYs@(l6R1Gm;Q@^^v4lC!|U%=o@?uyt?6H>{B-55l;^0FpAq2Yyg|~L8Q`B+ z{u2S-)$0uPPh7qB5nk%${1#WQ(=)rT*DoUKHOst|P2$?mm0F(J%Jb8FT)i$2@UC80 z1bA1kcz{2La!UE<{@A8lsOjIN{AYe+iF1`7jDAb_$9Gx6&8N30e{UfC2r44sf3CdQ z|0n8?lpp+Co6f&9{cCb;_z&&2gbRNfj)tT&Pk9&q_sZA&&W2aF#H+(GknpX_JHJQX zp(gw-%A0dDqJEj#U-&1LH|G%W7cj7e|63saZOUiAZu52N&%yzc@E=y*rGF}pknokt zv)nVEpC}(J=Xe}D3BN+aoAWVZ48Xw?KA7)1<@5L0bX|C-w>$q}d8fCZqoE`n=Vv>; zjj~tYBzy*MgKd|syuc@H{61tl;v{dU>YIzQ)ui(wFX-3&)bOVy{XiG~_wfDQ4<8nu z!2T)aU-MT>epqF3%^5ar@oKuu*Buce^bNPJZlM; z{;5<}+NY1UkJIO|$}jC<%TuZ8B$U7MK})#t&K|2tXX0kdyLR|5c>e41IROQda_)b{ zif+&lweV7&D}QK-V&!jFe!Q;tKC1j<%KyEeEhj(C=S{vy`Uieyf1BqZ_?pQ=LDK(N zwhiynzgT&GZqN5((w6)9}SlSYo*HcWe0B_gg}%=lxjuo!_#&yn{kM z@=g*d&$&DL>7VWI znDT#x4{PW7bdcm*s{MjFGM{zI7wdR;?f-)E!SVZ!^1s#onxyHBWx|s5SMIZfYqxgg zm&nVr`CMqlp1hwx+WEFXf9=umQ~KKQuD<6C4zJe=!}s(0zhD!-Ow)fBUdl7{ZA;8j z{tXTP_D?Kfo}J=r778HY-9C_OhtuGtoNiy|N=;|1;q`|PZiDaVo$3u;Ys>FMU(@vG zXurGmc_bp8927vxGrq&7;Pl}VzJ&GfVhz9JaU1?(&3Io#_zNjOSUL;fCEwSz-Q+I5 ze0D2;gZ7In&!}7*{XU{KE{;tp1@ccBNTb19IW5c_2HYwlnunjj`!~YUKtbP8X;kT$h zyLLG3Ok18ybslu}O~8x(WU66s{O6Rf+-5WEZ;R!P5H~zap+L~4dsj9v_z5e zIYYwR?Nay?rA2dX`q>(O2E6Fo7M+)imH)2tf4|-mu77_Ie}<%^`))_d%PE7PE&^8e6$Pgnk)w=D5- z&?U}?KUw`p0kv{JHxhfzK!P1Rep*34`(QU zzw%FNI`U3I`RsrXE6*iEZN99yrq19?O*4YUd9(!W=-0_)e-!& z@Y4Qwb=uJDHB!cTmfx)9cjGake9(@4QTe*S_ao8JsA!qdu^Y_){bp8-sirXA*6UZ(-C06wgoU(@izbX?FK=JT-fFRA@} zRrO)eaGU<_CvCU}4L?QsJ=a+xU->U7U+dWfT>B5Wz@}4ol?~Xa;V)Ev^JgtFLit6? zx5^&{;ge8)qw>M^R8F2v|CztofX+UguYAYbmY<;MOo11DzGICgzPNSH9^z%im$e%gXP0))G^cKcIZcK1(pH%;zHoHk}^YFEqRP zd{y~XI(~_6J})Zo&f#46eafGyegbCAd`>E~=?B-3Wy+tt*oJfM)2#fMfd95!`O-i; zZ&to3U_KcMUGdMh^1KS=n`1k!nt@G_3J-fjb4sNw&v=}c?0gtIe!X(&l& z&kro`+O1gmZEsutLz>Pi<@2Aj#31FjD&O%ZOSp0IhVsF6M9+~n-`qEBc;|<-D*xD% zmUsHU1YYWOiViHz#`~3qU-e*m_J=n zc^15Dl%Mm6CC*m<&wKeso9@k;e*cSXzC)g{gv+Z;`M+yBWA@GG3gs_XyP@WQzj1BT z>GvNt9PY9C9E5jiDrKPm#WsBQLCb2fyphTW{oYS1KS2FSH=o|6{5oxCH{ZPge+G7A z&|2F*y)^STG<@cMON>#zj4#sull0z!EAx%Y->l_4O~XH={NTqeLARRED`Udz`&W2L z|8{MkN*ml8dr5fs)rMz0ZnFU~Yv!{HUeXEr;eS*9kow_HAI6LgPyZHpNhdfDKBN44 zwHr*k=2JXQ)>B)ZoI67m-N?ZyM0)re%plan$8XlKSamxNgCv!@bDdr->Q7!VoRK?{2EDLzx&|_8vaqWS2I=k zj`E2POE`a^cbP3u!<&{rLBkJIe)m(BI79hF1b>(E!TpNwD?c|g!=&KS$-30$`$yF` z)ZTnPr~IzJ+TXbM=JOxQ2lw|5D!)<(+?g8w%<drX#@B<&zxsd;=*sho^6$K7d1r6WsIcjmyleTn znHFz>59^m-YxuW6ZNvT9f_Khj3E$O^nhY=f*h|OD`5OM9^6jb*G^6>PKE;L)t_!YH z{-%d)IH$L3mEW_)@|RoiQns!=Vi{<@D!#DiM@=hPl__$5yCDs44 zHT`JV}JLx;2ltY zr>+O*D}Q#CO@E9k&RNP|tNe1UZ-3>VRQ|Q6EpdkOh0|?$da2*OO8HuNY3HlAS|VTh zB^rLo=Pa>W`G02EbnahbiG=ds)pSalE#dn8Z_4+2JDu+}!{$3&_lG~J>0F`wq`z^Q;U9Uq@1RfE^j}f`)Ai#tb=Dh4ga==pS#Zz&Mpn3 z^9g{dDPEdyQ@94(zUc+&JB*;m=mP?Z)-|%WS@D|762o zV8L6ie75?5&i?-i-jz**8aR)R4&yI`mwE;Lv4rx$_5N~rX*YLId5~suw@JUBckVYV z@wnwH?Q{4bOb?l0|& zNdH162&vbsfId{gOZ(JmJAY8qe+WLToEcZx@WK7D0{F1-l@a`RHJw3?Hp4SD-<`?_ z?N4sp=6k>Dzsu`0@G`Dft+ENo-};cxcQpKGwO%g#ua$rCGd7&d_qaJW{VSAr;VYCM zuKP$MY&7qB3=-s$0NwVnTFfP|CnpBlw4f&+OxQWBj3crvbdQ^VWlwclrJ!BAp5IZ1~`OdYAIwQTu$F zX0sh$^xr*iu_W6H{-WX6Z%)tmbS5&{hYX&xp9?Skd2YZzp93%D5BmAHD&IrPGepbt zn)1Qhk)dNk=35U^L@F(rJ6bk~gdDD&_zFLra{m{5O@Kpnif&|M$u-Q++#E!)IO{gNFj6C}RQ~nF zmN;4Yrb{p91y^MOy>bXMqm>iDaa5AM&efO4i4L?EoZBJXm)$xAi-_(TFYM;g_rQ|dC4_4nm0ki-~OB>+&FEAm-}+TdrCKG z_?sTF;m^}_9#nq&qn5Z%`JXC3SljL6%AdeE?8o@psn16!?G1o;ZRI$@%i*P77wNz{ zTZK27@aRuqzP?Yxj|$lBU%<;fCHI`B8%O&z{Q8G%!W%UG6PWL%-L8Gf67qg=`CJMw zCiT={d+clmtKg7=E@TXlWv z{N7U9MbdxjF&pqq4gXc;CsJPd^tIsqN%{ZM{Q=B~`F!|#n@(_leunbFdy-p}AAP{a z|GB1<*=zrPYBP@86m2c3Ftx*2T zi0}_9zwU7xaF~YQru@sA{z&CBZVaz)CA`!tXiu(FK6s!1cI8+9z$QFN)8C_f(;qFd zRr%i71+g>1^~lNat_?M)feG+Zo{Ya(*4eif<>Q+z@7no`$}jnz4=JDXtR0PT|KL8Gz=fL5KU_M0vV`+9 z2i|1MGgSR*H-48Ye--vpK1CM1Un(EGXWkS23#;!;cxmUITWtd8YdSY6A6zeNReqb= z8JBO*Wj398YKL1josTIWycaan@qe}f&(iSsE5G=!miVypyWqwC52u~vGu?vsPYwS% zc11p}e~UkF%k!ezbJvga9Ix}HYv-Gkze5%0WSg9KD|}eH{Z7NLS3Bv(*O|+0zPlc_ z3Cz}X#wtHr^L6!&E5G&oHlUkF{;7Q4JC>)K=5xar!prjvy!7LEO~=`R0V^#3=YZWS zRzCQ=(&fq@()rqz=j+N}sr7R0^L^z#&3A~FGvgMUZ_p3 zEuSqp7C!+m>3lw6e}Bb-cby5ZKYZ{t4gY}7Yv-!$B~AapPD{w&50TFWD{cCjZ`t4H zSn$RxzqQd4XDk0j<%9do+u&W@HK>8T8ouM7mUUrH`;x71@P7V<@KWE~H2e%r>1hogybu49^1o`e;U{Z4XRgxv?zM!| z=W^wP=L<8HU)*NH|4`Fe125&-xyuqs_Z`KPehJ=Bjwv79C#+R|8vdPpTzh^|`K7vU zb^7@;<%9dHC*E%J4esZZz{`G@dk;%Ln~gU`!?&ni`jqltfOln3%D^`?{5^lMtZV;Q zlwYRy;bM)ns;_OI;B#uHepTCF{WpS{&r`}5{)hejLFG?hZNtC(jwPJ_d`$Td?6dqZ z4Zl$Nk|s;Ix~x?`Xir{MeoSE84f>kRH+au*BD_mevox?-!|#2-vS+C9ZTMXM?uWj2 z*z}j`JsdSF-Z*&CtA#h(&~c5_WO#oH_WS2FoxT6GzbiG!hKTUbYWU@<|IV&vGXOeC2yK0oRYS*4TW5`<6E;pRvV;@37$I zeBFi*K3{n;ytLaI^ie*}{?r)Wa{m86rs=#M)Mw>`&mj)D%jUarlTAgN&#O~Dcy4}| z^3Md)e@^+`dT!$E()oAW^q+J-1 z+233HjSQ2}APe5*@S+b7>%8Xr@n+@cw430`ee8Rbf4ScB4_ok_Qa*SO=MT!ie3cEj z--36-H*CJ`S+*b*%9kh~ypMgk^0x*2nJ>YMKDhT=p-$%WT@8QeY5UvN>#Tch_-8g) zKAvH5F}$=x@Z9`vWumc2lQ%asq>=byp5GW*jxWu47;(6g33U4;|ibNvDJ`zJK~ zJD;({naa!ZP1?U+JMhqPRBzNm-g(?ajk0V-KqQw z88#i~hdiPDAHHYH>HLaq%0JU!dDj<*;6)z_zhn95EO@i-v-t+^za`*ZTn%dA2N8Vc z{WhH~YB$Ddq(nASuDL4S(%evAb4?jVn^GK)x3sl4 zj7%&_)GUxP6tpEi-Ho)Rx;0ul5@~!vQ@1G;IeDN}#cJv(B@J5N-d0{yR#naa_)M8JqoJk@r7Ej2y*RR!hF_S7*Uu~WwP%ck zo0?l2(Y1Qiuo`_UudF$y49lV`P*oByGo>k;9==iYA-TFpuDc4u_X3?sTsPd6fM!UjLD{%5U-k`P2=mMqIkTyr7ecyx3nf| zO+S@KxAiggoh?Dxl&ZXVEH7Rz21)WNpB^tOt16YQ5=DzMF#nr%NJ^mf^_L^Mx+&Ev z6XiOZ7VM^*j-?T!`Zc0e$Nx`g)oz-g8%;GWJtQL=HdvZ}GR8=0Y)e;J(rZPxX_ zb!7avH&snT`IM@7_-rFXn9tnlwkeak)Qxf;OQ%JR&%^82rAMM)|A$H|I#>38UFW)~ z>`~Z_!wuPPa(O=$;D~keaOHhp{Wm)$kIVo6p^2$#&o6FeIndk~udb;{w6!IDOS1qv zysiE})bY}||4)pi!!>x9rHN`P4X}`<;ic0qnOqhxn^GFbu*AntnGr9WpyY(oXd%=85mrbmccFUW`qOt87HWeCOZ3|nf+v|$r73?$=#mm`8 zz$Gn+#|q4MR@1Vt&`MFJN&JHL1#C3LAsg97D4&I&MZpbOKC!fgg&QHQ=QYiGKT_M+ z$n@@t*OQIwvyk4TN0L2RMoA{e>)Y72m|s0VQR~_wmbaj({?iK*@s{>hyoZ0MYNauo z=p@ym654ERb9JlDE7^v5v|)b16uNCbdnZ-R4Q3aiiVmS%QVX~9VB4&mT1d0W0{y7k zZ4#5D?becJRdUx>=(QwkRJ?4eafwLXsf89FdHPxmD$ZW#+?q^kg?F} zr&!TP8pCG`f*U9K4ef|)s`5>1`5LRBDRFhYy1|TfhNkvG*$A`Q)qG7k?ORs1u+sLG z)3BlzHfXvGYgS4v)%C6A_4Tq(VgY7P3Z;k#=>t3}k7#NwH$5@X39uqMGW4ngtDP z(M?Fy&#%*JgoPY2Pd1O*$1zkInwy{NKqe{nHN*VHB0!D~n~l{HpQ zl5Hwcb!#^p+9M2R1MR|MK?YqsWn7t!WGo7cS(_eNY#vSmpLKSx{GCFQxof4Eel&(0{h$^D{>ohZf_4u z94o4=pQkI}xLN&DUHR{@9v30BwN$LY?nF}T z)v8FUsu6LSMcm92*sc`KE{fATWo7=hOiuc)eImLjH5;NmV{r=NsY$V{j7FRs zr{q^RW7c)nOpSlzLxc^#^lmo}5!;f%t=o|`4T8*mf&)B1 zVKv;Arc0V`B^ZpVU8iKDwYdRJNoTM@lqG?#OaCO35*rbM#jWd}$~V~!ixoNPerjQLC! zjK)cB{1)OK`g0KfBqVyEIsGw+5aLJ0N^0v{%^Z78vV(9##pvY4jgzGoCqn%r+ogz| z_qIft-=L60mwkCAr$QR9YGhY`R4nIV2Y3l~!niU?4?~@3;OrJrSX=`+()~{+c?c(m%lroH(pxbT2|rLmi%~y?<_2z)e*)d-NY8lj1?=g&csZP z?P}$)hgqi0{BX{$SPI`zk~5UhS=7~tO31`3)1!oQ?#~EK*0Nn)3>QA=-NXvqyjdmA zqq7jn0X5Q2I?ePO=U+PYL~5E?*z{Ilb(x^z)uv9O59ub=ndHbBEHW$XOIxCqMH$m~ zmn_XHH9{fN3s6?lSiLCTR)4LWfT_*J7!~SVJ1@G%Wx>iy6;o=-Y);9dhhAjG7!%gn z3R^U&+huoA%sMc{Z2Fd;fi)+GldM_uIq@;8()4yS3L9>SeJMsLdN=V{U>!>{n6Wx- z()9Qgz7<=y*9{F!qggbYMK3G#vWesnX0v<+4>SeMoZ7HZyO-*{SYQoOXL>umhl62) zx*f}_O}N3uIwL>sb{5nr4UA}YN2rLKp4H5|p&MN~P3efrNQ>C@kZ(ODuDe@L`Em&} ztbt0V3QPUOz)DhL3>i!XN4TzY6`xe%))ZD6 zl{V00u1!+o+t^FgB-Adr;AxXo9+Q+9TbZO{fk`T`N;Fm0L_|&WYpl>+EvWi;m)u>a zl0volmDR1{s71~sN3=7>=9t|d8y72YSSTwyJs#5SuKcoz#tS}{&6h&EAEIqi(%x(v zBW>mC^Asq-w0bltxhLa112@@8k;>$@i&?*>)+B7y`IampmNCqsY-tJ3Ss23dsoeOe zt#67iXiL;;KH#-OQjVvrbIg7U9w#f2_WV3d?bYnmB|UC;h9I(_(^}Kq za!t9}{l&bhLrXOwyDXn7DLV(*d2uqrF&-g0A*~aSx3h73HA~}KyXlkWL70kFC-;EV ze->+>Tt}LfrgMpP-!bJ+jBIb#%R5KDMpdsQxlPIZVszh}Tpq=K?NpWzwrBivQ?Xp< zTF}M_V@Jg~T4tgOEYi({9BttOy&SRwlCsXOdDLr+vVy)rDC>LQ{AIj!6vfP8zA}Z) z5jxIOxyv|E9n`r&7q|I&&g2_Yud5~5EOw^f3~|>;f!&Qr7s|Nx3*%R}R=2byS~1O{ zXV#;`pTm!hFRV{o&4P*9D#Qo!RWC59;08Fqnw4EsfH#cD1E+I}@tPget;Usv76 zm9=?XsB0H}phuHy>eNEu7;4W>UvjA>+goNEQdXnUqdP5gS0pr1Z<@Vg4KoL5(&%b<3Ogzm^RoEq}t_obE&*pTGWm2R{-gW=9RjJ+LK zo=*!z?aE0O=VJU@8dws=+hu9WIOalNb=>Tu#MvaOsT;!)+9hMlhFwM`MAGrYA<&fj(+ur)9T~8V@5bVIEIhu`$#+t@1 zz9%{oScc#tvA^wGRdm07ldjIpY*VpEs!6df9eC8oWW`MObiZy0l-PX;a|Wko)|_e9 z*XacHe(r=!Wjz&QSJO<4*te9k7NzXcnCy1TSrFHsWx?+>exYxz9kx=gWJH+Nh<{jw z`V7R@gFG=SaoMdvIb3JZ&7lj1fOYF;x=fA$Iqm%S#^JG_80)CaFECS1gPV2A7S%K? zz|WnJJDt)5Z4TpD1?uVMaddyOJEP;sMU*JoB>3HmuIDsjY~R0w=ah$&-q`^&nsOu~ zRL@QHUANL=MO=vF^7JR_+v}=u<;>1ea?y_3`Hv^esaRumeG@H2GY~O!qqfX7Na2omqN#c=7eKhlI;*YK1m$YR+-us+m0Mhj zh1CrU*qANjX$x}#DGMn%_Vc+-VV-E%*-7$pf{eMeBhbJwk3Q?_dLUXPXN)*jhq;1A zmfeoXa5HQ@=fGx5X`XI=`+UljN;nhcb6~sVt?QvwiNy5V6?SscKFsW%yyPVMm_DSK zJ`Up~b%~KOTU}=Ue{ah0y8(6uZ8n4^u54qE&2$RcTPrI4n-$ya_!r$F#EgstCE*GD*aN*SYi>u7DR<()t>iUKl z-`FdVhkM%Op$xOPe5GuR)?A70l5q9&xfAB$4sshT%|pbPh0GIOk5=NfZO!pIZfZ6p zv_eO;h;%y!qEtqe)9rMutX>>XcI=`JuPhIehBs(b8(RJut*if1 zKT&QR9nHXqsB~nx?2W+m#f~oEk2Wc@=;*p2HM$+vlVO|LDR1ZqS1l=bTz2@3AMZbh zVYQGvsJ7l$#Bzvc_9vC6N%`E+a2t|t#*^JkS;SVG(Are7=?44-wwKIR;I0o{$^8?1 z_t#ecXmv`AL5OntjG~s*najvwA2|YAD$2qS;N4ISti1dU=2MTr0>IbOVfL|$g~`uL zJ?cBXxw5&9yGzYn$<#xlfZ90sPYy(6$uqsht!10thDq>3n)-C;56b{|*b}dg$I2^x zWmUbExy7Dz;2CqZl$lrh&sW{B(Qu5{1>^vW-O1`U*&d0{!<7m8CYa5%Lo%u`qsiO_ z<>HIE+1_n4skh`WexSG9o?rM4pGx2D9+{Nl>^?97@z{FMbDqf9Fo#j8D5akm&NGO^ zS(+|d6lY!9CQm5v7(;v^&*huF&i8ZYiu{d}>Km@U-j(~|Q3DlIvPORy^}4Al3EFgQ9)Rs`XH`-TfSDP5Lo1-0{{HjpXMTJl zTPIR+bN3;2|BU85+Ulzx8B%G{_pT3!giYPFOY+G9V!dx#9n0r#I#)R7CYXQ{c24Dj z5t=qVUV(Aq@)|8WedZJfro8ij*Jdn?$&PAG11806Ik5FQuemIE-qF?dj1?yDv45hz z7B#SwcKU{mS`(^BpXpk(c|NaLh+2w9cdc_et#w6@AhumAsXY09e8m=KJTfJ z=MN5dPsOi5X=kFzJ3YQUXg79mnijCv)50q}@OjJS{B14<4IAB1KUa=?xUUhHJ;9`% zE-19h9i}U$l;%YNAv-!{w)AW+1!dx%9b-cKs3TOmhwIn5<&T7xg`8a|BoO zCTPU=CWK?_@oz+H? z=`KyJOGwsuGrp%eg_s4_VG@(aNbZMF$lEYu_j7#g$Hfe_`M}2B&u2xyI%}Os`)xyRHk4xewni_Y% z<;@Ht$8}uRPA%BneC?Em$w1EX-Kh8PGIcX?2X68CY(+PbQbsOnF`V4NN?*OoLfHA8 zmrch>$w7DK`RmKqBo4sj$aSlb7tuVb!6IEfV{wg zTUE>_E*tZHm?C>1z$Gg8SSW@d#&pirDtENvYam8K7h`%%J1{=8>$8Y<^`VZqKKUju z(Bf7YyKt1lJ+t3wnw#&?`DiVXi6eC4<2X{ zJ?`qj8Sg1=TlH{?bZj@=@d+yNitG%AQj4Z{JqZ(slxH*IZM;^koR`k<^fPmro`;G0 zyAv_@$VB?Ikn(n{IZKwH&T_=1l8QJ>$I-|zd zW(;>%@1s>Q_vBUA^@^F5WXQDg7K!G0!S#6=Zmd2h%h0j!Ai`{=oSEopv8?Ry?|mt@ zu9NujYP*Bw(OJZnLm#syOxO}DcoUC8PL@MhE_BITjAV{8x4{CBVaesycy-wIRfaYO z!fxweiHfY-17*fi+I!VXHcbC0h?mZ``myo+4<^&I`+U1c^9{Yo;dD3E8`;!C7ndGUzo98r?5K5pkW+4LMBnacl&xR6-65H1*V;y-TBIs8vW0DZbDf(V;0Oz%oj84FP`_;Zgn2+(l*p|^V)~u9qqRdv;?TLb z?u4(uwHa7@`@1gb)g^6qmunk(SZRhr`c)Vv9NIaoHGFp`^rkz{j248wlt~6NX@u;~ zj0jw!2{Az?8)tzJGnXCP+DRAO##54FraMYu7fEZlzJ&kN^_ExD)oJH6qDsCZ2XWsbLCQ=UqbQ>%EUQnS~TZ^c+p>koP?@(j7Y;=nD9Nj%n z*V+EbrAv#DX7DLNOco@E!?It``m(#H1jVqYE;yz(&wM9UNB5w90q3t_dz{#ppzk7$ z;@_nUvEj$~0_$443Okl3XNq~hxm2MsO@Z~GFGv<}~1?gk^HD(MIrUp3JENp~sw8c3HU0#i4I2|fnl zw*%?7o7Blny6i_-JiAWmvN{SF{$f3O$I2U}HSf?0-2Mp;<$#3`G*8&Q8`|UFdGVlJ zMrK`Qw_%ULyQ*0zmXf|qzLDSeBxlf`6`SfF{isQ9zy^+|%o&h-6w*9M!z&89e~@M@ zE2Q*yW{IPEbzOZ;9Vb{_9-9d>txn-mOIW+C7gt?RyRB**LvxBB)pbLp8y~MV0dR31KaCs~0(<4&YQn#r7=$EQyo zr!OA%9dJf0nPSDMn@jd~j@g0e&b>-@Skg!Jg$^@sN2I`MXN*U;_mR3-5qgxaW3|hH zI&XW?1EZ)N>9B>C-OG>{v+~~GD&CCf9|of0hiQysya37p)%$;%)@2;I{Us*dY_3~{Ud+*C%U+P-8G}l{oQJTJspAl%tW0H>i`!0$@fy2^@rBjy_CYdG*HWww%cTn`Xa0{CiaM4F78`Dns@E^ zjZYWHKV`SsFL!>@5x-U=W)4!5bxnKOEoQX)*J?0-x!;MCA#JFOc8-%_$Cdr${lXvnC z|2Evz1rh)IBFs*ZIoggL#e1jlXw6B#u^{i;s}+nMaGYZQdVw<%QG4L_iD2!^=}~`9 zAjHK??>4)LKaAhtR$R%+o2GZ&Ql^ighTL}YJw~p(r}fRDw-3r{7S0XRCv)BM@Liic zRh$ys50AyFo9cO;dtzLop+R23!LFEI66dr~uBy~wi)s_Kwo!QTVL`r37{2xhm~yjA zFMY$n;^wB_udsjX!%j6v>o)Oxm(i#6ehIv!NNo<2hwmANqs>XT=0h`L+U@poVbYNo zuhS@h;E4u#cU@ck;T~$}VoXEqQ}~{ry~8NEByTawoly7oCEu&_yT2H!_d@lisNDQ2 zYmloF<~k)8-pobQklUkjlA!lM?W_5vx4Ss;Zg!U~le9b3smpRk*Cq1dHvNxrwG!&X z``*%bjMpN#QpjqfOjh(}L2s&`U&`N4yu=K#=GNkQjHL1=(dML~z(O9amYP3cAb)j3 z3z|A&=G-Xx0s(eo%XxVKXUl0D-9B|Xf-P@r#3^q25te&a6TwlQRf%`QAFDC%noI5) zx)qPURNtkZY9!}S$lj&@q*m8JzEz<6dlvyqb984@;(w&ufFqg&j_ouxrH%3oP4%LX zXJ{fketF`}Ki!)P_{M&uA4wm}i!PcK>W98B9duP}z3n=3eo}G2oLc9N;8Wzs$&<*Oe76~!Eu{`N1-~Ng5&eGno zlK20$b}b=tWo7s}<6~5GMo}1az=jziqA9vMNvDH?Jsn$YhKZq*L3c%^yV70Q)zwo~ zolHbT!HqJ4J0Hwq1VPYEwyvCw8$roRblmht2(D(OJLC7i?)lHT=iaJYNs-REum9e2 z&pqdVKemU7SDcvsknc4oj$eDFHoSu+r4h5A)wGirsB)$mF6#155^^cOmzQ3Nu_B+s zCiMh;wK#c9`eJ6ItDmfiT<(SC_~vbVKGNY8kK_7IaD*YRs)p5&_B5FZm@zFv<-oem z_4iIbjN#{u($^&w)alB)7&656Cy7AU__c%4GSEA0mGllDoEA0Fyx>yh#|&gPb^Th* zda!V8K~c4s`A*;xh49xT`5P8Y&g-|S z`cU5Omt8EW6V5}C6A>6j66vSO@z*QS4a(MaIS&|12X#=LqzBGxDK?5Mm=v#C&F^mn z!Es9_&UM$~^rse?L%FseBj|==JcxT71?F+y+gTvA~e{^elp-NPv_;d95sYN@&+w-$s5E*_m)YOWxm$gy(m$s+P$QM z4O75SQ7TNQma)0o*EKQKF-@Si$+$aXV#mDqMdC&9AuL(u+=r+=u<0Snc@B9skT+P3 zuFPhfE!30)Rg2Y9{#5Eck}%#a!d^?BGR@T|2;|8Gu3-U`E~Sb8%jLcC2;Tar=*J}m z!TR)WO{XPO3OWN~ajPsQH%7k6MgPdU}O3kb*~7UXQc zt$|3_S+lwl&`DYH8O0L-wq*KAxy zXu@D}2kCs{)xcOF3}0IJ1_|s|D}pe+W64OUrawCq!c}ah5PLl=LT>!pmuI9oSl?mA zqb^~Ym5gLs9d(oJsydwL=@arBG75$qf2o2k z%mF-nIsU#Xh3LwLYx(shr(s#y3)sFc9o}M7t3n{nB9pUByNVNFl?>MHo0#>Z@xE5E5aXO!sG;f0#$V)^8`klu5u$~LX&8!oRqc^516gYOZ=ewM z5f`JjB#J@vlUh0uNGFyK7CLc@Y=soOf7WTh)}pION4g6ux<(iwDPn$s;_FeAQ)&mnFxUcz%lDM~B66+U~6;!o`aec=u=`3_?#-ub=k4x1cbpl1n=FR9QB z)Ua!g9)f!F-^i`iN+DO%!Bk7Rf{?bwBbT7xnca(Nf~*bq!Zu?X@-aql?MNc}lhH#4 zZ;q1kdql*e1RB@OSEk$*V+Mt4@Q|{X@xc*93tXB^N|#N6#!-^OD!uX)KnN(}b#vxs zVnw{3nLUy~*vF&M_?Yu!>>xVQ*{ns83BM)8PQvQK#om$LmNRLvP>%n$RP;;u!emqk zxs&G4BtU(8FR9@Y8xxXkG3bx4J>c5LROFaGA**SFjM?qdHmM3wmQCJCSXWJJy(AxN zI>p!`t&vbD20xfwfx==sAZ7fLC=79`K|K{Jh9zIX>;om!tmZdY;-)<1S^0)21fHMX z!c#R}nT4kg>qi*nvSo?lxnYPz5mMZ_n)#ILtO`w8|3WGX7x@>$=QTrm(R(0|Y9avF zdm-@Baja3F3;@cW>GrV7-XPN!xTLf!z0R>p(>R?w-l|;vC39oF85AOKam-Z9E38#t37lGd0#o#L#TuZ7hWXe zMuaU55~q1Z%uveyuFGnl&&h(5hOiN8)b89QM~unf02()x7Pu2Rd7!6*)1&@`vs;P2 zj4eH)RbiAFSEcSaBT2%Hp3lz#CQMd-AqD{i<1x5>8|?q^&Oj9T^;x583{}F?G>L9h z4|gM*Q!#OeaRcD&d-?^IatXv7-v$F}o&fx*;0D-*8z71t2RA@UUkxQ$)$?y5Tv1{Z zWeAwjsis1rYTRV5pzKB4YWONAJ8MLXpa9sj6y4r`IwA8YzEnfzL1K~^Et7dn;O=&{g8Qpu zSY6=$Efb{9Gna^LY2($;!*S>c&MyA@EAG4#!u4(Ycc9 z9K5VIm)pZ4Y>R7v@QsjBEGE7Kx$AB_pNwwg7~JiiXkM-@Ah;%qP_IrLnvyIx>6j@} zrV0ay_o3R#!-WETHrNC)ZSSnPv61-(T1(-^-9}B|CXxUJpKd&}0yw-a8;w zh~(-w#GM4`QCWC^hCsU@^0w_V*4Q-+HE?QgS{5$HRD|{@eh*+K*0G+PA~Eir;beB& zKZ^8%#0}T+!aVoVFg???SX3eh8U&)-_D$BQ=KAqIxnaaTw}=s;0!wj4g5`}dqP+#; zBe3$bED{C-qaB%YtdQPDGG${Q-K3}a* zR?DQNN|K1CP|Y1W&0TQbi>)nCCz{!Xht|6oItJOsrcqy0;Vmk1u1-+gcn<=lpK1J} zt!ai$TMR~x&AB8g`EQL55ta>7(gcBA$O%t+A>35`_FSxl8 z^*%I4PQ4!!wslg_-kp%fXPd>PWvQFa9^_>%8S1u(fy1Wu`X~K8RBy_4)4R?Q+lW8pOy`4U(n;$(6#>e zB6dE6KZD;;cxzYv#n^r(*b(JlP}7J*1oH7)_;4Bwcp42|2g>g6uxW~p88MywEpYi;-}c4F}3@h=?AlR zMF-rTZ}9g1KUerIyWtmfL)#MtAo$PVFNOa#j?tLd{fkZP&~KCfFKB-IU;6#u!buOm zvME(|3d=*gIDB9*1q&- zd$P3lTs!(lZ212F=F_xS6xM!>_riba`27i|2;wQbWJTZ$ZU3h8xjbzf+WrNthyQy= z;H~{q(&6;^|4iVY`KrKMd$JWj(B_XF{Z|72#t#MF+V3UfpFaP;fTvfawL!mkekt(p z{8-u*J{tbB=Ue+UE_)WI4gO5w&lKLSvpd+nwa)^co@DS375<^ZKQusX4bBd~0AN}> z2LIq!bhn~-@DpjD(+^AQpMC~!pXK*B=AVD|mcX68Dea2gn4tr_-RJYT%)=LN3taIt zY0oK~H3nzI4ch`Pf zpT9x`3ct_c@5}hJ{u}=LFpec%#F3(S;~(;*zy6MQ%m3{8cHk4($iaX8N7Ab+2|%)2 F{2$J- 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 )