Files
nd-wfc/demos/nonogram/nonogram.cpp
2025-08-22 15:00:50 +09:00

303 lines
8.3 KiB
C++

#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;
}