303 lines
8.3 KiB
C++
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;
|
|
}
|