Files
nd-wfc/demos/console_renderer.cpp
2025-08-26 14:00:00 +09:00

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();
}