From 4b8d6602683d2944d54c12a68d5d46048ed79080 Mon Sep 17 00:00:00 2001 From: cdemeyer-teachx Date: Tue, 26 Aug 2025 07:47:51 +0000 Subject: [PATCH] 49fa333c-bfa3-47e6-9332-0826c9982565 --- demos/CMakeLists.txt | 21 ++ demos/README.md | 267 +++++++++++++++++ demos/console_renderer.cpp | 509 +++++++++++++++++++++++++++++++++ demos/console_renderer.h | 130 +++++++++ demos/sudoku_demo_renderer | Bin 0 -> 167360 bytes demos/sudoku_demo_renderer.cpp | 188 ++++++++++++ 6 files changed, 1115 insertions(+) create mode 100644 demos/CMakeLists.txt create mode 100644 demos/README.md create mode 100644 demos/console_renderer.cpp create mode 100644 demos/console_renderer.h create mode 100755 demos/sudoku_demo_renderer create mode 100644 demos/sudoku_demo_renderer.cpp diff --git a/demos/CMakeLists.txt b/demos/CMakeLists.txt new file mode 100644 index 0000000..9986095 --- /dev/null +++ b/demos/CMakeLists.txt @@ -0,0 +1,21 @@ +# 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 pthread for std::thread +find_package(Threads REQUIRED) +target_link_libraries(sudoku_demo_renderer Threads::Threads) + +# 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 new file mode 100644 index 0000000..e89050e --- /dev/null +++ b/demos/README.md @@ -0,0 +1,267 @@ +# 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 new file mode 100644 index 0000000..54ada16 --- /dev/null +++ b/demos/console_renderer.cpp @@ -0,0 +1,509 @@ +#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 new file mode 100644 index 0000000..d3c2262 --- /dev/null +++ b/demos/console_renderer.h @@ -0,0 +1,130 @@ +#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_demo_renderer b/demos/sudoku_demo_renderer new file mode 100755 index 0000000000000000000000000000000000000000..015743c97cd94a62dd5b476037670f3248b5e246 GIT binary patch literal 167360 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 F{2$J- literal 0 HcmV?d00001 diff --git a/demos/sudoku_demo_renderer.cpp b/demos/sudoku_demo_renderer.cpp new file mode 100644 index 0000000..e08575a --- /dev/null +++ b/demos/sudoku_demo_renderer.cpp @@ -0,0 +1,188 @@ +#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; +}