From 4f1d7c524744861ee9cd3052c2ec9e42e83a32ca Mon Sep 17 00:00:00 2001 From: Loic Guegan Date: Tue, 25 Jan 2022 10:53:10 +0100 Subject: Refactoring --- CMakeLists.txt | 6 +- src/HalfMove.cpp | 66 ++++++++++++ src/HalfMove.hpp | 38 +++++++ src/PGN.cpp | 265 +++++++++++++++++++++++++++++++++++++++++++++ src/PGN.hpp | 81 ++++++++++++++ src/pgnp.cpp | 322 ------------------------------------------------------- src/pgnp.hpp | 99 ----------------- 7 files changed, 454 insertions(+), 423 deletions(-) create mode 100644 src/HalfMove.cpp create mode 100644 src/HalfMove.hpp create mode 100644 src/PGN.cpp create mode 100644 src/PGN.hpp delete mode 100644 src/pgnp.cpp delete mode 100644 src/pgnp.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 237f223..2381751 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,13 +2,15 @@ cmake_minimum_required(VERSION 3.10) project(pgnp) # Shared library -add_library(pgnp SHARED src/pgnp.cpp) +add_library(pgnp SHARED src/PGN.cpp src/HalfMove.cpp) # Includes set(PGNP_INCLUDE_DIR ${CMAKE_CURRENT_BINARY_DIR}/includes) # For conveniance set(PGNP_INCLUDE_DIR ${PGNP_INCLUDE_DIR} PARENT_SCOPE) # To be used by other projects with add_subdirectory() file(MAKE_DIRECTORY ${PGNP_INCLUDE_DIR}) -configure_file(src/pgnp.hpp ${PGNP_INCLUDE_DIR} COPYONLY) +configure_file(src/PGN.hpp ${PGNP_INCLUDE_DIR}/pgnp.hpp COPYONLY) +configure_file(src/HalfMove.hpp ${PGNP_INCLUDE_DIR} COPYONLY) + include_directories(${PGNP_INCLUDE_DIR}) # Unit tests diff --git a/src/HalfMove.cpp b/src/HalfMove.cpp new file mode 100644 index 0000000..7c7a85e --- /dev/null +++ b/src/HalfMove.cpp @@ -0,0 +1,66 @@ +#include "HalfMove.hpp" + +namespace pgnp { + +HalfMove::HalfMove() : count(-1), isBlack(false), MainLine(NULL) {} + +HalfMove::~HalfMove() { + for (auto *move : variations) { + delete move; + } +} + +std::string HalfMove::NestedDump(HalfMove *m, int indent) { + std::stringstream ss; + + for (int i = 0; i < indent; i++) { + ss << " "; + } + ss << " " + << " Move=" << m->move << " Count=" << m->count << " Comment=\"" + << m->comment << "\"" + << " IsBlack=" << m->isBlack << " Variations=" << m->variations.size() + << std::endl; + + for (auto *var : m->variations) { + ss << NestedDump(var, indent + 1); + } + + if (m->MainLine != NULL) { + ss << NestedDump(m->MainLine, indent); + } + return (ss.str()); +} + +std::string HalfMove::Dump() { return (NestedDump(this, 0)); } + +int HalfMove::GetLength() { + int length = 0; + HalfMove *m = this; + while (m != NULL) { + length++; + m = m->MainLine; + } + return length; +} + +void HalfMove::Copy(HalfMove *copy) { + copy->count = count; + copy->isBlack = isBlack; + copy->move = move; + copy->comment = comment; + + // Copy MainLine + if (MainLine != NULL) { + copy->MainLine = new HalfMove(); + MainLine->Copy(copy->MainLine); + } + + // Copy variation + for (HalfMove *var : variations) { + HalfMove *new_var = new HalfMove(); + copy->variations.push_back(new_var); + var->Copy(new_var); + } +} +} // namespace pgnp \ No newline at end of file diff --git a/src/HalfMove.hpp b/src/HalfMove.hpp new file mode 100644 index 0000000..dc238cd --- /dev/null +++ b/src/HalfMove.hpp @@ -0,0 +1,38 @@ +#include +#include +#include + +namespace pgnp { + +/** + * Most members are public for conveniance sake + */ +class HalfMove { +private: + /// @brief Recursive dump + std::string NestedDump(HalfMove *, int); + +public: + /// @brief Contains current move count + int count; + /// @brief Is this move for black + bool isBlack; + /// @brief The SAN move + std::string move; + /// @brief Comment associated to the move + std::string comment; + /// @brief Next HalfMove link to this line + HalfMove *MainLine; + /// @brief Next HalfMove links to variation of this line + std::vector variations; + + HalfMove(); + ~HalfMove(); + /// @brief Get number of HalfMove in the MailLine + int GetLength(); + /// @brief Dump move and all its variations + std::string Dump(); + /// @brief Perform a deep copy of a HalfMove + void Copy(HalfMove *copy); +}; +} // namespace pgnp \ No newline at end of file diff --git a/src/PGN.cpp b/src/PGN.cpp new file mode 100644 index 0000000..e77bcde --- /dev/null +++ b/src/PGN.cpp @@ -0,0 +1,265 @@ + +#include "pgnp.hpp" +#include +#include + +#define IS_BLANK(c) (c == ' ' || c == '\n' || c == '\t') +#define IS_DIGIT(c) \ + (c == '0' || c == '1' || c == '2' || c == '3' || c == '4' || c == '5' || \ + c == '6' || c == '7' || c == '8' || c == '9') +#define IS_EOF(loc) (loc >= pgn_content.size()) +#define EOF_CHECK(loc) \ + { \ + if (IS_EOF(loc)) \ + throw UnexpectedEOF(); \ + } + +namespace pgnp { + +PGN::~PGN() { + if (moves != NULL) + delete moves; +} + +std::string PGN::GetResult() { return (result); } + +void PGN::FromFile(std::string filepath) { + std::ifstream file(filepath); + + std::string content((std::istreambuf_iterator(file)), + std::istreambuf_iterator()); + FromString(content); +} + +void PGN::FromString(std::string pgn_content) { + this->pgn_content = pgn_content; + moves = NULL; + int loc = 0; + while (!IS_EOF(loc)) { + char c = pgn_content[loc]; + if (!IS_BLANK(c)) { + if (c == '[') { + loc = ParseNextTag(loc); + } else if (IS_DIGIT(c)) { + moves = new HalfMove(); + loc = ParseHalfMove(loc, moves); + break; + } + } + loc++; + } + if (result.size() <= 0) { + throw InvalidGameResult(); + } +} + +void PGN::STRCheck() { + int i = 0; + // Locate Event tag + while (i < tagkeys.size()) { + if (tagkeys[i] == "Event") { + break; + } + i++; + } + + // Check tags + if (i + 6 < tagkeys.size()) { + bool valid = (tagkeys[i] == "Event") && (tagkeys[i + 1] == "Site") && + (tagkeys[i + 2] == "Date") && (tagkeys[i + 3] == "Round") && + (tagkeys[i + 4] == "White") && (tagkeys[i + 5] == "Black") && + (tagkeys[i + 6] == "Result"); + if (!valid) { + throw STRCheckFailed(); + } + } else { + throw STRCheckFailed(); + } +} + +bool PGN::HasTag(std::string key) { + auto tags = GetTagList(); + return (std::find(tags.begin(), tags.end(), key) != tags.end()); +} + +int PGN::ParseHalfMove(int loc, HalfMove *hm) { + // Goto next char + loc = NextNonBlank(loc); + EOF_CHECK(loc); + char c = pgn_content[loc]; + + // Check if we reach score entry (* or 1-0 or 0-1 or 1/2-1/2) + if (!IS_EOF(loc + 1)) { + char nc = pgn_content[loc + 1]; // Next c + if ((IS_DIGIT(c) && nc == '-') or (IS_DIGIT(c) && nc == '/') or c == '*') { + if (c == '*') { + result = "*"; + } else if (nc == '-') { + if (c == '1') { + result = "1-0"; + } else { + result = "0-1"; + } + } else { + result = "1/2-1/2"; + } + return (loc); + } + } + + // Parse (move number + if (IS_DIGIT(c)) { + std::string move_nb; + while (IS_DIGIT(c)) { + move_nb += c; + loc++; + c = pgn_content[loc]; + EOF_CHECK(loc); + } + hm->count = std::stoi(move_nb); + loc++; + EOF_CHECK(loc); + if (pgn_content[loc] == '.') { + hm->isBlack = true; + loc += 2; // Skip two dots + EOF_CHECK(loc); + } + } else { + hm->isBlack = true; + } + + // Parse the HalfMove + loc = NextNonBlank(loc); + EOF_CHECK(loc); + c = pgn_content[loc]; + std::string move; + while (!IS_BLANK(c) && c != ')') { + move += c; + loc++; + c = pgn_content[loc]; + EOF_CHECK(loc); + } + hm->move = move; + + // Skip end of variation + if (c == ')') { + loc++; + return (loc); + } + + // Check for comment + loc = NextNonBlank(loc); + if (!IS_EOF(loc) && pgn_content[loc] == '{') { + loc++; // Skip '{' + c = pgn_content[loc]; + while (c != '}') { + hm->comment += c; + loc++; + EOF_CHECK(loc); + c = pgn_content[loc]; + } + loc++; // Skip '}' + } + + // Check for variations + loc = NextNonBlank(loc); + while (!IS_EOF(loc) && pgn_content[loc] == '(') { + loc++; // Skip '(' + HalfMove *var = new HalfMove; + loc = ParseHalfMove(loc, var); + hm->variations.push_back(var); + loc++; // Skip ')' + } + + // Parse next HalfMove + loc = NextNonBlank(loc); + if (!IS_EOF(loc)) { + HalfMove *next_hm = new HalfMove; + next_hm->count = hm->count; + loc = ParseHalfMove(loc, next_hm); + // Check if move parsed successfuly + if (next_hm->move.size() > 0) { + hm->MainLine = next_hm; + } else { + delete next_hm; + } + } + + return (loc); +} + +int PGN::ParseNextTag(int start_loc) { + // Parse key + std::string key; + int keyloc = start_loc + 1; + EOF_CHECK(keyloc); + char c = pgn_content[keyloc]; + while (!IS_BLANK(c)) { + key += c; + keyloc++; + EOF_CHECK(keyloc); + c = pgn_content[keyloc]; + } + + // Parse value + std::string value; + int valueloc = NextNonBlank(keyloc) + 1; + EOF_CHECK(keyloc); + c = pgn_content[valueloc]; + while (c != '"' or IS_EOF(valueloc)) { + value += c; + valueloc++; + EOF_CHECK(keyloc); + c = pgn_content[valueloc]; + } + + // Add tag + tags[key] = value; + tagkeys.push_back(key); + + EOF_CHECK(valueloc + 1); + c = pgn_content[valueloc + 1]; + if (c != ']') { + throw UnexpectedCharacter(c, ']', valueloc + 1); + } + + return (valueloc + 1); // +1 For the last char of the tag which is ']' +} + +void PGN::GetMoves(HalfMove *copy) { moves->Copy(copy); } + +std::vector PGN::GetTagList() { return tagkeys; } + +std::string PGN::GetTagValue(std::string key) { + if (tags.find(key) == tags.end()) { + throw InvalidTagName(); + } + return tags[key]; +} + +std::string PGN::Dump() { + std::stringstream ss; + ss << "---------- PGN DUMP ----------" << std::endl; + ss << "Tags:" << std::endl; + for (auto &tag : GetTagList()) { + ss << " " << tag << "=" << GetTagValue(tag) << std::endl; + } + ss << "Moves:" << std::endl; + + if (moves != NULL) + ss << moves->Dump(); + return (ss.str()); +} + +int PGN::NextNonBlank(int loc) { + char c = pgn_content[loc]; + while (IS_BLANK(c)) { + loc++; + if (IS_EOF(loc)) + return (loc); + c = pgn_content[loc]; + } + return (loc); +} + +} // namespace pgnp \ No newline at end of file diff --git a/src/PGN.hpp b/src/PGN.hpp new file mode 100644 index 0000000..6da955b --- /dev/null +++ b/src/PGN.hpp @@ -0,0 +1,81 @@ +#include "HalfMove.hpp" +#include +#include +#include +#include + +namespace pgnp { + +class PGN { +private: + /// @brief Contains tags data + std::unordered_map tags; + /// @brief Contains the tags list in parsed order + std::vector tagkeys; + /// @brief Contains game result (last PGN word) + std::string result; + /// @brief COntains the parsed PGN moves + HalfMove *moves; + /// @brief Contains the PGN data + std::string pgn_content; + +public: + ~PGN(); + void FromFile(std::string); + void FromString(std::string); + /// @brief Check if PGN contains a specific tag + bool HasTag(std::string); + /// @brief Perform a Seven Tag Roster compliance check + void STRCheck(); + /// @brief Dump parsed PGN into a string + std::string Dump(); + /// @brief Retrieve parsed tag list + std::vector GetTagList(); + /// @brief Access to the value of a tag + std::string GetTagValue(std::string); + /// @brief Get game result based on the last PGN word + std::string GetResult(); + /// @brief Fetch PGN moves as HalfMove structure + void GetMoves(HalfMove *); + +private: + /// @brief Populate @a tags with by parsing the one starting at location in + /// argument + int ParseNextTag(int); + /// @brief Get the next non-blank char location starting from location in + /// argument + int NextNonBlank(int); + /// @brief Parse a HalfMove at a specific location into @a pgn_content + int ParseHalfMove(int, HalfMove *); +}; + +struct UnexpectedEOF : public std::exception { + const char *what() const throw() { return "Unexpected end of pgn file"; } +}; + +struct InvalidTagName : public std::exception { + const char *what() const throw() { return "Invalid tag name"; } +}; + +struct InvalidGameResult : public std::exception { + const char *what() const throw() { return "Invalid game result"; } +}; + +struct UnexpectedCharacter : public std::exception { + std::string msg; + UnexpectedCharacter(char actual, char required, int loc) { + std::stringstream ss; + ss << "Expected \'" << required << "\' at location " << loc + << " but read \'" << actual << "\'"; + msg = ss.str(); + } + const char *what() const throw() { return msg.c_str(); } +}; + +struct STRCheckFailed : public std::exception { + const char *what() const throw() { + return "Seven Tag Roster compliance check failed"; + } +}; + +} // namespace pgnp diff --git a/src/pgnp.cpp b/src/pgnp.cpp deleted file mode 100644 index 3262a5e..0000000 --- a/src/pgnp.cpp +++ /dev/null @@ -1,322 +0,0 @@ - -#include "pgnp.hpp" -#include -#include - -#define IS_BLANK(c) (c == ' ' || c == '\n' || c == '\t') -#define IS_DIGIT(c) \ - (c == '0' || c == '1' || c == '2' || c == '3' || c == '4' || c == '5' || \ - c == '6' || c == '7' || c == '8' || c == '9') -#define IS_EOF(loc) (loc >= pgn_content.size()) -#define EOF_CHECK(loc) \ - { \ - if (IS_EOF(loc)) \ - throw UnexpectedEOF(); \ - } - -namespace pgnp { - -HalfMove::HalfMove() : count(-1), isBlack(false), MainLine(NULL) {} - -HalfMove::~HalfMove() { - for (auto *move : variations) { - delete move; - } -} - -void HalfMove::NestedDump(HalfMove *m, int indent) { - for (int i = 0; i < indent; i++) { - std::cout << " "; - } - std::cout << " " - << " Move=" << m->move << " Count=" << m->count << " Comment=\"" - << m->comment << "\"" - << " IsBlack=" << m->isBlack - << " Variations=" << m->variations.size() << std::endl; - - for (auto *var : m->variations) { - NestedDump(var, indent + 1); - } - - if (m->MainLine != NULL) { - NestedDump(m->MainLine, indent); - } -} - -void HalfMove::Dump() { NestedDump(this, 0); } - -int HalfMove::GetLength() { - int length = 0; - HalfMove *m = this; - while (m != NULL) { - length++; - m = m->MainLine; - } - return length; -} - -void HalfMove::Copy(HalfMove* copy){ - copy->count=count; - copy->isBlack=isBlack; - copy->move=move; - copy->comment=comment; - - // Copy MainLine - if(MainLine!=NULL){ - copy->MainLine=new HalfMove(); - MainLine->Copy(copy->MainLine); - } - - // Copy variation - for(HalfMove *var:variations){ - HalfMove *new_var=new HalfMove(); - copy->variations.push_back(new_var); - var->Copy(new_var); - } -} - -PGN::~PGN() { - if (moves != NULL) - delete moves; -} - -std::string PGN::GetResult() { return (result); } - -void PGN::FromFile(std::string filepath) { - std::ifstream file(filepath); - - std::string content((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - FromString(content); -} - -void PGN::FromString(std::string pgn_content) { - this->pgn_content = pgn_content; - moves = NULL; - int loc = 0; - while (!IS_EOF(loc)) { - char c = pgn_content[loc]; - if (!IS_BLANK(c)) { - if (c == '[') { - loc = ParseNextTag(loc); - } else if (IS_DIGIT(c)) { - moves = new HalfMove(); - loc = ParseLine(loc, moves); - break; - } - } - loc++; - } - if (result.size() <= 0) { - throw InvalidGameResult(); - } -} - -void PGN::STRCheck() { - int i = 0; - // Locate Event tag - while (i < tagkeys.size()) { - if (tagkeys[i] == "Event") { - break; - } - i++; - } - - // Check tags - if (i + 6 < tagkeys.size()) { - bool valid = (tagkeys[i] == "Event") && (tagkeys[i + 1] == "Site") && - (tagkeys[i + 2] == "Date") && (tagkeys[i + 3] == "Round") && - (tagkeys[i + 4] == "White") && (tagkeys[i + 5] == "Black") && - (tagkeys[i + 6] == "Result"); - if (!valid) { - throw STRCheckFailed(); - } - } else { - throw STRCheckFailed(); - } -} - -bool PGN::HasTag(std::string key) { - auto tags = GetTagList(); - return (std::find(tags.begin(), tags.end(), key) != tags.end()); -} - -int PGN::ParseLine(int loc, HalfMove *hm) { - // Goto next char - loc = NextNonBlank(loc); - EOF_CHECK(loc); - char c = pgn_content[loc]; - - // Check if we reach score entry (* or 1-0 or 0-1 or 1/2-1/2) - if (!IS_EOF(loc + 1)) { - char nc = pgn_content[loc + 1]; // Next c - if ((IS_DIGIT(c) && nc == '-') or (IS_DIGIT(c) && nc == '/') or c == '*') { - if (c == '*') { - result = "*"; - } else if (nc == '-') { - if (c == '1') { - result = "1-0"; - } else { - result = "0-1"; - } - } else { - result = "1/2-1/2"; - } - return (loc); - } - } - - // Parse (move number - if (IS_DIGIT(c)) { - std::string move_nb; - while (IS_DIGIT(c)) { - move_nb += c; - loc++; - c = pgn_content[loc]; - EOF_CHECK(loc); - } - hm->count = std::stoi(move_nb); - loc++; - EOF_CHECK(loc); - if (pgn_content[loc] == '.') { - hm->isBlack = true; - loc += 2; // Skip two dots - EOF_CHECK(loc); - } - } else { - hm->isBlack = true; - } - - // Parse the HalfMove - loc = NextNonBlank(loc); - EOF_CHECK(loc); - c = pgn_content[loc]; - std::string move; - while (!IS_BLANK(c) && c != ')') { - move += c; - loc++; - c = pgn_content[loc]; - EOF_CHECK(loc); - } - hm->move = move; - - // Skip end of variation - if (c == ')') { - loc++; - return (loc); - } - - // Check for comment - loc = NextNonBlank(loc); - if (!IS_EOF(loc) && pgn_content[loc] == '{') { - loc++; // Skip '{' - c = pgn_content[loc]; - while (c != '}') { - hm->comment += c; - loc++; - EOF_CHECK(loc); - c = pgn_content[loc]; - } - loc++; // Skip '}' - } - - // Check for variations - loc = NextNonBlank(loc); - while (!IS_EOF(loc) && pgn_content[loc] == '(') { - loc++; // Skip '(' - HalfMove *var = new HalfMove; - loc = ParseLine(loc, var); - hm->variations.push_back(var); - loc++; // Skip ')' - } - - // Parse next HalfMove - loc = NextNonBlank(loc); - if (!IS_EOF(loc)) { - HalfMove *next_hm = new HalfMove; - next_hm->count = hm->count; - loc = ParseLine(loc, next_hm); - // Check if move parsed successfuly - if (next_hm->move.size() > 0) { - hm->MainLine = next_hm; - } else { - delete next_hm; - } - } - - return (loc); -} - -int PGN::ParseNextTag(int start_loc) { - // Parse key - std::string key; - int keyloc = start_loc + 1; - EOF_CHECK(keyloc); - char c = pgn_content[keyloc]; - while (!IS_BLANK(c)) { - key += c; - keyloc++; - EOF_CHECK(keyloc); - c = pgn_content[keyloc]; - } - - // Parse value - std::string value; - int valueloc = NextNonBlank(keyloc) + 1; - EOF_CHECK(keyloc); - c = pgn_content[valueloc]; - while (c != '"' or IS_EOF(valueloc)) { - value += c; - valueloc++; - EOF_CHECK(keyloc); - c = pgn_content[valueloc]; - } - - // Add tag - tags[key] = value; - tagkeys.push_back(key); - - EOF_CHECK(valueloc + 1); - c = pgn_content[valueloc + 1]; - if (c != ']') { - throw UnexpectedCharacter(c, ']', valueloc + 1); - } - - return (valueloc + 1); // +1 For the last char of the tag which is ']' -} - -void PGN::GetMoves(HalfMove* copy) { moves->Copy(copy); } - -std::vector PGN::GetTagList() { return tagkeys; } - -std::string PGN::GetTagValue(std::string key) { - if (tags.find(key) == tags.end()) { - throw InvalidTagName(); - } - return tags[key]; -} - -void PGN::Dump() { - std::cout << "---------- PGN DUMP ----------" << std::endl; - std::cout << "Tags:" << std::endl; - for (auto &tag : GetTagList()) { - std::cout << " " << tag << "=" << GetTagValue(tag) << std::endl; - } - std::cout << "Moves:" << std::endl; - - if (moves != NULL) - moves->Dump(); -} - -int PGN::NextNonBlank(int loc) { - char c = pgn_content[loc]; - while (IS_BLANK(c)) { - loc++; - if (IS_EOF(loc)) - return (loc); - c = pgn_content[loc]; - } - return (loc); -} - -} // namespace pgnp \ No newline at end of file diff --git a/src/pgnp.hpp b/src/pgnp.hpp deleted file mode 100644 index 492dfc0..0000000 --- a/src/pgnp.hpp +++ /dev/null @@ -1,99 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace pgnp { - -class HalfMove { -private: - /// @brief Recursive dump - void NestedDump(HalfMove *, int); - -public: - int count; - bool isBlack; - std::string move; - std::string comment; - HalfMove *MainLine; - std::vector variations; - - HalfMove(); - ~HalfMove(); - /// @brief Get number of HalfMove in the MailLine - int GetLength(); - /// @brief Dump move and all its variations - void Dump(); - void Copy(HalfMove* copy); -}; - -class PGN { -private: - std::unordered_map tags; - std::vector tagkeys; - std::string result; - - HalfMove *moves; - std::string pgn_content; - -public: - ~PGN(); - void FromFile(std::string); - void FromString(std::string); - bool HasTag(std::string); - /// @brief Perform a Seven Tag Roster compliance check - void STRCheck(); - /// @brief Dump parsed PGN - void Dump(); - std::vector GetTagList(); - std::string GetTagValue(std::string); - std::string GetResult(); - void GetMoves(HalfMove*); - -private: - /// @brief Populate @a tags with by parsing the one starting at location in - /// argument - int ParseNextTag(int); - - /// @brief Get the next non-blank char location starting from location in - /// argument - int NextNonBlank(int); - - int ParseLine(int, HalfMove *); -}; - -struct UnexpectedEOF : public std::exception { - const char *what() const throw() { return "Unexpected end of pgn file"; } -}; - -struct InvalidTagName : public std::exception { - const char *what() const throw() { return "Invalid tag name"; } -}; - -struct InvalidGameResult : public std::exception { - const char *what() const throw() { return "Invalid game result"; } -}; - -struct UnexpectedCharacter : public std::exception { - std::string msg; - UnexpectedCharacter(char actual, char required, int loc) { - std::stringstream ss; - ss << "Expected \'" << required << "\' at location " << loc - << " but read \'" << actual << "\'"; - msg = ss.str(); - } - const char *what() const throw() { return msg.c_str(); } -}; - -struct STRCheckFailed : public std::exception { - const char *what() const throw() { - return "Seven Tag Roster compliance check failed"; - } -}; - -} // namespace pgnp -- cgit v1.2.3