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