diff --git a/demos/nonogram/README.md b/demos/nonogram/README.md new file mode 100644 index 0000000..8fa8e4b --- /dev/null +++ b/demos/nonogram/README.md @@ -0,0 +1,151 @@ +# Nonogram Loader + +A memory-efficient C++ nonogram loader that stores nonogram solution and instructions in a separate class, using minimal memory while only storing important information. + +## Features + +- **Memory Efficient**: Uses compact storage for hints (uint8_t) and solutions (1 bit per cell) +- **Standard Format Support**: Reads `.non` format files as specified in the format specification +- **Ignores Unnecessary Data**: Only stores essential puzzle data, ignores author names and other metadata +- **Multiple Loading Methods**: Load from files, strings, or entire directories +- **Solution Support**: Optional solution storage when present in the file + +## Memory Usage + +The loader is designed to use minimal memory: + +- **Hints**: Stored as `uint8_t` arrays with compact indexing +- **Solutions**: Stored as bit arrays (1 bit per cell) +- **Dimensions**: Stored as `uint16_t` for puzzles up to 65535x65535 + +For a 10x10 puzzle: +- Row hints: ~10 bytes +- Column hints: ~10 bytes +- Solution: ~13 bytes (10x10 bits = 100 bits = 13 bytes) +- Total: ~33 bytes + overhead + +## Usage + +### Basic Usage + +```cpp +#include "nonogram.h" + +// Load a single nonogram from file +auto nonogram = NonogramLoader::fromFile("puzzle.non"); +if (nonogram) { + std::cout << "Loaded: " << nonogram->getWidth() << "x" << nonogram->getHeight() << std::endl; + + // Access row hints + for (size_t i = 0; i < nonogram->getRowCount(); ++i) { + auto hints = nonogram->getRowHints(i); + // Process hints... + } + + // Access column hints + for (size_t i = 0; i < nonogram->getColumnCount(); ++i) { + auto hints = nonogram->getColumnHints(i); + // Process hints... + } + + // Access solution if available + if (nonogram->hasSolution()) { + for (size_t row = 0; row < nonogram->getHeight(); ++row) { + for (size_t col = 0; col < nonogram->getWidth(); ++col) { + bool filled = nonogram->getSolutionCell(row, col); + // Process cell... + } + } + } +} +``` + +### Load from Directory + +```cpp +// Load all nonograms from a directory +auto puzzles = NonogramLoader::fromDirectory("/path/to/nonograms"); +for (const auto& puzzle : puzzles) { + // Process each puzzle... +} +``` + +### Load from String + +```cpp +std::string content = R"( +width 5 +height 5 + +rows +1 +2 +3 +4 +5 + +columns +1 +2 +3 +4 +5 + +goal 1000010000100001000010000 +)"; + +auto nonogram = NonogramLoader::fromString(content); +``` + +## Classes + +### Nonogram +Main class storing puzzle data: +- `getWidth()` / `getHeight()`: Puzzle dimensions +- `getRowHints(row)`: Get hints for a specific row +- `getColumnHints(col)`: Get hints for a specific column +- `hasSolution()`: Check if solution is available +- `getSolutionCell(row, col)`: Get solution cell value + +### NonogramLoader +Static utility class for loading: +- `fromFile(filename)`: Load from file +- `fromString(content)`: Load from string +- `fromDirectory(dirname)`: Load all .non files from directory + +## Storage Details + +### Hints Storage +- Uses `NonogramHintsStorage` class +- Each hint is a `uint8_t` (0-255) +- Compact storage with offset arrays +- Handles comma-separated hint sequences + +### Solution Storage +- Uses `NonogramSolutionStorage` class +- 1 bit per cell (filled/empty) +- Packed into bytes for efficiency +- Supports loading from goal strings with or without quotes + +## Supported Format + +The loader supports the standard `.non` format: +- Width/height dimensions +- Row and column hints (comma-separated numbers) +- Optional solution/goal string +- Ignores metadata (author, title, copyright, etc.) + +## Building + +```bash +# Compile with C++17 +g++ -std=c++17 -o myprogram main.cpp nonogram.cpp +``` + +## Example + +See `example.cpp` for a complete usage example that demonstrates: +- Loading from file +- Accessing hints and solutions +- Loading from directory +- Displaying puzzle information diff --git a/demos/nonogram/example.cpp b/demos/nonogram/example.cpp new file mode 100644 index 0000000..e654732 --- /dev/null +++ b/demos/nonogram/example.cpp @@ -0,0 +1,62 @@ +#include "nonogram.h" +#include + +int main() { + // Load a nonogram from file + auto nonogram = NonogramLoader::fromFile("/home/connor/repos/nd-wfc/demos/nonogram/data/db/webpbn/1.non"); + + if (!nonogram) { + std::cout << "Failed to load nonogram!" << std::endl; + return 1; + } + + std::cout << "Loaded nonogram: " << nonogram->getWidth() << "x" << nonogram->getHeight() << std::endl; + + // Display row hints + std::cout << "\nRow hints:" << std::endl; + for (size_t i = 0; i < nonogram->getRowCount(); ++i) { + const auto& hints = nonogram->getRowHints(i); + std::cout << "Row " << i << ":"; + for (uint8_t hint : hints) { + std::cout << " " << static_cast(hint); + } + std::cout << std::endl; + } + + // Display column hints + std::cout << "\nColumn hints:" << std::endl; + for (size_t i = 0; i < nonogram->getColumnCount(); ++i) { + const auto& hints = nonogram->getColumnHints(i); + std::cout << "Col " << i << ":"; + for (uint8_t hint : hints) { + std::cout << " " << static_cast(hint); + } + std::cout << std::endl; + } + + // Display solution if available + if (nonogram->hasSolution()) { + std::cout << "\nSolution:" << std::endl; + for (size_t row = 0; row < nonogram->getHeight(); ++row) { + for (size_t col = 0; col < nonogram->getWidth(); ++col) { + std::cout << (nonogram->getSolutionCell(row, col) ? "1" : "0"); + } + std::cout << std::endl; + } + } + + // Load all nonograms from a directory + std::cout << "\nLoading all nonograms from directory..." << std::endl; + auto puzzles = NonogramLoader::fromDirectory("/home/connor/repos/nd-wfc/demos/nonogram/data/db/webpbn"); + + std::cout << "Loaded " << puzzles.size() << " puzzles:" << std::endl; + for (size_t i = 0; i < puzzles.size(); ++i) { + std::cout << " Puzzle " << i << ": " << puzzles[i].getWidth() << "x" << puzzles[i].getHeight(); + if (puzzles[i].hasSolution()) { + std::cout << " (with solution)"; + } + std::cout << std::endl; + } + + return 0; +} diff --git a/demos/nonogram/nonogram.cpp b/demos/nonogram/nonogram.cpp new file mode 100644 index 0000000..c844f5d --- /dev/null +++ b/demos/nonogram/nonogram.cpp @@ -0,0 +1,302 @@ +#include "nonogram.h" +#include +#include +#include +#include +#include +#include + +Nonogram::Nonogram() : width_(0), height_(0) {} + +Nonogram::Nonogram(uint16_t w, uint16_t h) : width_(w), height_(h) {} + +void Nonogram::clear() { + width_ = 0; + height_ = 0; + row_hints_.clear(); + col_hints_.clear(); + solution_.reset(); +} + +bool Nonogram::loadFromFile(const std::string& filename) { + std::ifstream file(filename); + if (!file.is_open()) { + return false; + } + + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + return loadFromString(content); +} + +bool Nonogram::loadFromString(const std::string& content) { + clear(); + + std::istringstream iss(content); + std::string line; + int section = 0; // 0: header, 1: rows, 2: columns + + while (std::getline(iss, line)) { + if (!parseLine(line, section)) { + return false; + } + } + + // Validate that we have all required data + if (width_ == 0 || height_ == 0) { + return false; + } + + if (row_hints_.size() != height_ || col_hints_.size() != width_) { + return false; + } + + return true; +} + +bool Nonogram::parseLine(const std::string& line, int& section) { + std::string trimmed = trim(line); + + // Skip empty lines and comments + if (trimmed.empty() || trimmed[0] == '#') { + return true; + } + + // Skip lines we don't recognize (as per format spec) + if (startsWith(trimmed, "catalogue") || + startsWith(trimmed, "title") || + startsWith(trimmed, "by") || + startsWith(trimmed, "copyright") || + startsWith(trimmed, "license") || + startsWith(trimmed, "color")) { + return true; + } + + if (startsWith(trimmed, "width")) { + return parseDimensions(trimmed); + } + + if (startsWith(trimmed, "height")) { + return parseDimensions(trimmed); + } + + if (startsWith(trimmed, "rows")) { + section = 1; // Enter rows section + return true; + } + + if (startsWith(trimmed, "columns")) { + section = 2; // Enter columns section + return true; + } + + if (startsWith(trimmed, "goal")) { + return parseGoalLine(trimmed); + } + + // Check if this is a hints line (contains numbers) + if (std::any_of(trimmed.begin(), trimmed.end(), ::isdigit)) { + if (section == 1) { + return parseHintsLine(trimmed, true); // Parse as row hints + } else if (section == 2) { + return parseHintsLine(trimmed, false); // Parse as column hints + } + } + + return true; // Ignore unrecognized lines +} + +bool Nonogram::parseDimensions(const std::string& line) { + size_t space_pos = line.find(' '); + if (space_pos == std::string::npos) return false; + + std::string key = line.substr(0, space_pos); + std::string value = line.substr(space_pos + 1); + + try { + int dim = std::stoi(value); + if (dim <= 0 || dim > 65535) return false; + + if (key == "width") { + width_ = static_cast(dim); + } else if (key == "height") { + height_ = static_cast(dim); + } + return true; + } catch (const std::exception&) { + return false; + } +} + +bool Nonogram::parseHintsLine(const std::string& line, bool is_rows) { + std::vector hints = parseHintSequence(line); + if (hints.empty()) return true; // Empty line is OK + + if (is_rows) { + row_hints_.addHints(hints); + return true; + } else { + col_hints_.addHints(hints); + return true; + } +} + +bool Nonogram::parseGoalLine(const std::string& line) { + size_t space_pos = line.find(' '); + if (space_pos == std::string::npos) return false; + + std::string goal_str = line.substr(space_pos + 1); + goal_str = trim(goal_str); + + // Remove quotes if present + if (goal_str.length() >= 2 && goal_str[0] == '"' && goal_str[goal_str.length() - 1] == '"') { + goal_str = goal_str.substr(1, goal_str.length() - 2); + } + + if (goal_str.empty()) return false; + + solution_ = std::make_unique(); + return solution_->loadFromGoalString(goal_str, width_, height_); +} + +std::vector Nonogram::parseHintSequence(const std::string& hint_str) { + std::vector result; + std::string cleaned = hint_str; + + // Remove spaces and handle comma-separated values + cleaned.erase(std::remove_if(cleaned.begin(), cleaned.end(), ::isspace), cleaned.end()); + + std::istringstream iss(cleaned); + std::string token; + + while (std::getline(iss, token, ',')) { + // Skip empty tokens + if (token.empty()) continue; + + // Remove any non-digit characters from the end (like color codes) + size_t first_non_digit = 0; + for (; first_non_digit < token.size() && std::isdigit(token[first_non_digit]); ++first_non_digit) {} + token = token.substr(0, first_non_digit); + + if (token.empty() || !std::isdigit(token[0])) continue; + + try { + int num = std::stoi(token); + if (num > 0 && num <= 255) { // Max hint value + result.push_back(static_cast(num)); + } + } catch (const std::exception&) { + // Skip invalid numbers + } + } + + return result; +} + +// NonogramLoader implementation +std::optional NonogramLoader::fromFile(const std::string& filename) { + Nonogram nonogram; + if (nonogram.loadFromFile(filename)) { + return nonogram; + } + return std::nullopt; +} + +std::optional NonogramLoader::fromString(const std::string& content) { + Nonogram nonogram; + if (nonogram.loadFromString(content)) { + return nonogram; + } + return std::nullopt; +} + +std::vector NonogramLoader::fromDirectory(const std::string& dirname) { + std::vector puzzles; + + try { + for (const auto& entry : std::filesystem::directory_iterator(dirname)) { + if (entry.is_regular_file()) { + std::string filename = entry.path().string(); + std::string extension = ".non"; + if (filename.size() >= extension.size() && + filename.substr(filename.size() - extension.size()) == extension) { + if (auto nonogram = fromFile(filename)) { + puzzles.push_back(std::move(*nonogram)); + } + } + } + } + } catch (const std::filesystem::filesystem_error&) { + // Directory doesn't exist or can't be read + } + + return puzzles; +} + +std::vector NonogramLoader::splitContent(const std::string& content) { + std::vector puzzles; + std::istringstream iss(content); + std::string line; + std::string current_puzzle; + + while (std::getline(iss, line)) { + if (trim(line) == "====") { + if (!current_puzzle.empty()) { + puzzles.push_back(current_puzzle); + current_puzzle.clear(); + } + } else { + current_puzzle += line + "\n"; + } + } + + if (!current_puzzle.empty()) { + puzzles.push_back(current_puzzle); + } + + return puzzles; +} + +// Utility functions +std::string trim(const std::string& str) { + size_t first = str.find_first_not_of(" \t\r\n"); + if (first == std::string::npos) return ""; + size_t last = str.find_last_not_of(" \t\r\n"); + return str.substr(first, last - first + 1); +} + +bool startsWith(const std::string& str, const std::string& prefix) { + return str.substr(0, prefix.length()) == prefix; +} + +std::vector split(const std::string& str, char delimiter) { + std::vector result; + std::istringstream iss(str); + std::string token; + + while (std::getline(iss, token, delimiter)) { + result.push_back(trim(token)); + } + + return result; +} + +std::vector parseNumberSequence(const std::string& str, char delimiter) { + std::vector result; + std::vector tokens = split(str, delimiter); + + for (const std::string& token : tokens) { + if (!token.empty()) { + try { + int num = std::stoi(token); + if (num > 0 && num <= 255) { + result.push_back(static_cast(num)); + } + } catch (const std::exception&) { + // Skip invalid numbers + } + } + } + + return result; +} diff --git a/demos/nonogram/nonogram.h b/demos/nonogram/nonogram.h new file mode 100644 index 0000000..aca9d47 --- /dev/null +++ b/demos/nonogram/nonogram.h @@ -0,0 +1,180 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +// Forward declarations +struct NonogramHints; +struct NonogramSolution; + +// Compact storage for nonogram hints +// Stores each hint as a uint8_t (max hint value 255) +class NonogramHintsStorage { +public: + // Each hint group is stored as: [count, hint1, hint2, ..., hintN] + std::vector data; + std::vector offsets; // Start position of each row/column in data + + void clear() { + data.clear(); + offsets.clear(); + } + + // Add a new row/column's hints + void addHints(const std::vector& hints) { + offsets.push_back(static_cast(data.size())); + data.push_back(static_cast(hints.size())); // Store count first + for (uint8_t hint : hints) { + data.push_back(hint); + } + } + + // Get hints for a specific row/column + std::vector getHints(size_t index) const { + if (index >= offsets.size()) return {}; + + size_t start = offsets[index]; + if (start >= data.size()) return {}; + + uint8_t count = data[start]; + std::vector result(count); + for (size_t i = 0; i < count; ++i) { + result[i] = data[start + 1 + i]; + } + return result; + } + + size_t size() const { return offsets.size(); } +}; + +// Compact bit storage for nonogram solution +// Each cell is 1 bit (filled=1, empty=0) +class NonogramSolutionStorage { +public: + std::vector data; // 8 bits per byte + uint16_t width = 0; + uint16_t height = 0; + + void resize(uint16_t w, uint16_t h) { + width = w; + height = h; + size_t total_bits = static_cast(w) * h; + size_t total_bytes = (total_bits + 7) / 8; // Ceiling division + data.resize(total_bytes, 0); + } + + void clear() { + data.clear(); + width = 0; + height = 0; + } + + // Get cell value at (row, col) - true if filled, false if empty + bool get(size_t row, size_t col) const { + if (row >= height || col >= width) return false; + size_t bit_index = row * width + col; + size_t byte_index = bit_index / 8; + size_t bit_offset = bit_index % 8; + return (data[byte_index] & (1 << (7 - bit_offset))) != 0; + } + + // Set cell value at (row, col) + void set(size_t row, size_t col, bool filled) { + if (row >= height || col >= width) return; + size_t bit_index = row * width + col; + size_t byte_index = bit_index / 8; + size_t bit_offset = bit_index % 8; + if (filled) { + data[byte_index] |= (1 << (7 - bit_offset)); + } else { + data[byte_index] &= ~(1 << (7 - bit_offset)); + } + } + + // Load from goal string (sequence of 0s and 1s) + bool loadFromGoalString(const std::string& goal, uint16_t w, uint16_t h) { + size_t expected_size = static_cast(w) * h; + if (goal.length() != expected_size) return false; + + resize(w, h); + for (size_t i = 0; i < expected_size; ++i) { + if (goal[i] == '1') { + size_t row = i / w; + size_t col = i % w; + set(row, col, true); + } + } + return true; + } +}; + +// Memory-efficient nonogram class +class Nonogram { +public: + Nonogram(); + explicit Nonogram(uint16_t w, uint16_t h); + + // Dimensions + uint16_t getWidth() const { return width_; } + uint16_t getHeight() const { return height_; } + + // Hints access + std::vector getRowHints(size_t row) const { + return row_hints_.getHints(row); + } + + std::vector getColumnHints(size_t col) const { + return col_hints_.getHints(col); + } + + size_t getRowCount() const { return row_hints_.size(); } + size_t getColumnCount() const { return col_hints_.size(); } + + // Solution access (if available) + bool hasSolution() const { return solution_ != nullptr; } + bool getSolutionCell(size_t row, size_t col) const { + return solution_ ? solution_->get(row, col) : false; + } + + // Load from various formats + bool loadFromFile(const std::string& filename); + bool loadFromString(const std::string& content); + + void clear(); + +private: + uint16_t width_ = 0; + uint16_t height_ = 0; + NonogramHintsStorage row_hints_; + NonogramHintsStorage col_hints_; + std::unique_ptr solution_; + + // Parser helpers + bool parseLine(const std::string& line, int& section); + bool parseHintsLine(const std::string& line, bool is_rows); + bool parseGoalLine(const std::string& line); + bool parseDimensions(const std::string& line); + std::vector parseHintSequence(const std::string& hint_str); +}; + +// Fast nonogram loader +class NonogramLoader { +public: + static std::optional fromFile(const std::string& filename); + static std::optional fromString(const std::string& content); + static std::vector fromDirectory(const std::string& dirname); + +private: + static std::vector splitContent(const std::string& content); +}; + +// Utility functions +std::string trim(const std::string& str); +bool startsWith(const std::string& str, const std::string& prefix); +std::vector split(const std::string& str, char delimiter); +std::vector parseNumberSequence(const std::string& str, char delimiter); diff --git a/demos/nonogram/test_nonogram.cpp b/demos/nonogram/test_nonogram.cpp new file mode 100644 index 0000000..acfb291 --- /dev/null +++ b/demos/nonogram/test_nonogram.cpp @@ -0,0 +1,118 @@ +#include "nonogram.h" +#include +#include +#include + +void printNonogram(const Nonogram& nonogram) { + std::cout << "Nonogram: " << nonogram.getWidth() << "x" << nonogram.getHeight() << std::endl; + + std::cout << "Row hints:" << std::endl; + for (size_t i = 0; i < nonogram.getRowCount(); ++i) { + const auto& hints = nonogram.getRowHints(i); + std::cout << " Row " << i << ":"; + for (uint8_t hint : hints) { + std::cout << " " << static_cast(hint); + } + std::cout << std::endl; + } + + std::cout << "Column hints:" << std::endl; + for (size_t i = 0; i < nonogram.getColumnCount(); ++i) { + const auto& hints = nonogram.getColumnHints(i); + std::cout << " Col " << i << ":"; + for (uint8_t hint : hints) { + std::cout << " " << static_cast(hint); + } + std::cout << std::endl; + } + + if (nonogram.hasSolution()) { + std::cout << "Solution:" << std::endl; + for (size_t row = 0; row < nonogram.getHeight(); ++row) { + for (size_t col = 0; col < nonogram.getWidth(); ++col) { + std::cout << (nonogram.getSolutionCell(row, col) ? "1" : "0"); + } + std::cout << std::endl; + } + } +} + +int main() { + std::cout << "Testing Nonogram Loader..." << std::endl; + + // Test loading from file + std::string filepath = "/home/connor/repos/nd-wfc/demos/nonogram/data/db/webpbn/1.non"; + std::cout << "Attempting to load from: " << filepath << std::endl; + + std::ifstream test_file(filepath); + if (test_file.is_open()) { + std::cout << "File exists and can be opened." << std::endl; + std::string content((std::istreambuf_iterator(test_file)), + std::istreambuf_iterator()); + std::cout << "File content length: " << content.length() << std::endl; + std::cout << "File content:\n" << content << std::endl; + test_file.close(); + + // Try to load from this content + auto nonogram = NonogramLoader::fromString(content); + if (nonogram) { + std::cout << "Successfully loaded nonogram from file content!" << std::endl; + printNonogram(*nonogram); + } else { + std::cout << "Failed to load nonogram from file content." << std::endl; + } + } else { + std::cout << "Cannot open file!" << std::endl; + } + + auto nonogram = NonogramLoader::fromFile(filepath); + if (nonogram) { + std::cout << "Successfully loaded nonogram from file!" << std::endl; + printNonogram(*nonogram); + } else { + std::cout << "Failed to load nonogram from file." << std::endl; + } + + std::cout << "\nTesting with sample data..." << std::endl; + + // Test with sample data + std::string sample_data = + "width 5\n" + "height 10\n" + "\n" + "rows\n" + "2\n" + "2,1\n" + "1,1\n" + "3\n" + "1,1\n" + "1,1\n" + "2\n" + "1,1\n" + "1,2\n" + "2\n" + "\n" + "columns\n" + "2,1\n" + "2,1,3\n" + "7\n" + "1,3\n" + "2,1\n" + "\n" + "goal 01100011010010101110101001010000110010100101111000\n"; + + auto sample_nonogram = NonogramLoader::fromString(sample_data); + if (sample_nonogram) { + std::cout << "Successfully loaded nonogram from string!" << std::endl; + printNonogram(*sample_nonogram); + } else { + std::cout << "Failed to load nonogram from string." << std::endl; + } + + // Test loading from directory + std::cout << "\nTesting directory loading..." << std::endl; + auto puzzles = NonogramLoader::fromDirectory("/home/connor/repos/nd-wfc/demos/nonogram/data/db/webpbn"); + std::cout << "Loaded " << puzzles.size() << " puzzles from directory." << std::endl; + + return 0; +}