02b4a43d-1ad5-405f-a491-829d44f0bfd9

This commit is contained in:
cdemeyer-teachx
2025-08-22 15:00:50 +09:00
parent 5d3836dd01
commit f9371ce0b5
5 changed files with 813 additions and 0 deletions

151
demos/nonogram/README.md Normal file
View File

@@ -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

View File

@@ -0,0 +1,62 @@
#include "nonogram.h"
#include <iostream>
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<int>(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<int>(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;
}

302
demos/nonogram/nonogram.cpp Normal file
View File

@@ -0,0 +1,302 @@
#include "nonogram.h"
#include <iostream>
#include <fstream>
#include <filesystem>
#include <algorithm>
#include <cctype>
#include <sstream>
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<char>(file)),
std::istreambuf_iterator<char>());
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<uint16_t>(dim);
} else if (key == "height") {
height_ = static_cast<uint16_t>(dim);
}
return true;
} catch (const std::exception&) {
return false;
}
}
bool Nonogram::parseHintsLine(const std::string& line, bool is_rows) {
std::vector<uint8_t> 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<NonogramSolutionStorage>();
return solution_->loadFromGoalString(goal_str, width_, height_);
}
std::vector<uint8_t> Nonogram::parseHintSequence(const std::string& hint_str) {
std::vector<uint8_t> 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<uint8_t>(num));
}
} catch (const std::exception&) {
// Skip invalid numbers
}
}
return result;
}
// NonogramLoader implementation
std::optional<Nonogram> NonogramLoader::fromFile(const std::string& filename) {
Nonogram nonogram;
if (nonogram.loadFromFile(filename)) {
return nonogram;
}
return std::nullopt;
}
std::optional<Nonogram> NonogramLoader::fromString(const std::string& content) {
Nonogram nonogram;
if (nonogram.loadFromString(content)) {
return nonogram;
}
return std::nullopt;
}
std::vector<Nonogram> NonogramLoader::fromDirectory(const std::string& dirname) {
std::vector<Nonogram> 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<std::string> NonogramLoader::splitContent(const std::string& content) {
std::vector<std::string> 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<std::string> split(const std::string& str, char delimiter) {
std::vector<std::string> result;
std::istringstream iss(str);
std::string token;
while (std::getline(iss, token, delimiter)) {
result.push_back(trim(token));
}
return result;
}
std::vector<uint8_t> parseNumberSequence(const std::string& str, char delimiter) {
std::vector<uint8_t> result;
std::vector<std::string> 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<uint8_t>(num));
}
} catch (const std::exception&) {
// Skip invalid numbers
}
}
}
return result;
}

180
demos/nonogram/nonogram.h Normal file
View File

@@ -0,0 +1,180 @@
#pragma once
#include <string>
#include <vector>
#include <optional>
#include <cstdint>
#include <array>
#include <memory>
#include <utility>
// 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<uint8_t> data;
std::vector<uint16_t> 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<uint8_t>& hints) {
offsets.push_back(static_cast<uint16_t>(data.size()));
data.push_back(static_cast<uint8_t>(hints.size())); // Store count first
for (uint8_t hint : hints) {
data.push_back(hint);
}
}
// Get hints for a specific row/column
std::vector<uint8_t> 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<uint8_t> 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<uint8_t> 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<size_t>(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<size_t>(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<uint8_t> getRowHints(size_t row) const {
return row_hints_.getHints(row);
}
std::vector<uint8_t> 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<NonogramSolutionStorage> 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<uint8_t> parseHintSequence(const std::string& hint_str);
};
// Fast nonogram loader
class NonogramLoader {
public:
static std::optional<Nonogram> fromFile(const std::string& filename);
static std::optional<Nonogram> fromString(const std::string& content);
static std::vector<Nonogram> fromDirectory(const std::string& dirname);
private:
static std::vector<std::string> 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<std::string> split(const std::string& str, char delimiter);
std::vector<uint8_t> parseNumberSequence(const std::string& str, char delimiter);

View File

@@ -0,0 +1,118 @@
#include "nonogram.h"
#include <iostream>
#include <vector>
#include <fstream>
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<int>(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<int>(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<char>(test_file)),
std::istreambuf_iterator<char>());
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;
}