From 80c5dd4d1d9b72c19f1dfc09b7482374020b3619 Mon Sep 17 00:00:00 2001 From: Johannes Czech Date: Thu, 22 Oct 2020 00:51:47 +0200 Subject: [PATCH] Upgrade ffish.js to 0.4.4 and add validate_fen * Upgraded ffish.js to 0.4.4 + validate_fen(const std::string& fen, const Variant* v) (Closes #87) + ffish.validate_fen(fen) + ffish.validate_fen(fen, uciVariant) + ffish.startingFen(uciVariant) + board.pocket(turn) Refactoring + replaced bool first = true by save_pop_back(string& s) + replaced string = " " + s; by string += " "; string += s; updated README.md + simplified memory specification * Added C++17 specification for building ffish.js + updated ffish.js to 0.4.4 * Added more checks to validate_fen() + FEN_TOUCHING_KINGS + replaced FEN_INVALID_CHECK_COUNTER by FEN_INVALID_NUMBER_OF_KINGS + combined FEN_FILE_SIZE_MISMATCH and FEN_INVALID_NUMBER_OF_RANKS into FEN_INVALID_BOARD_GEOMETRY + added more checks for FEN_INVALID_CASTLING_INFO added struct CharSquare added class CharBoard (for geometry checks) refactored different checks into separate functions --- src/apiutil.h | 531 ++++++++++++++++++++++++++++++++++++++++++++++++- src/ffishjs.cpp | 406 ++++++++++++++++++++------------------ src/variant.cpp | 1 + src/variants.ini | 5 +- tests/js/README.md | 30 ++- tests/js/package.json | 2 +- tests/js/test.js | 70 +++++++- 7 files changed, 835 insertions(+), 210 deletions(-) diff --git a/src/apiutil.h b/src/apiutil.h index 1d185ea..8334eaf 100644 --- a/src/apiutil.h +++ b/src/apiutil.h @@ -15,9 +15,19 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ +#include +#include +#include +#include +#include +#include + +#include "types.h" +#include "variant.h" + namespace PSQT { - void init(const Variant* v); +void init(const Variant* v); } enum Notation { @@ -169,8 +179,8 @@ Disambiguation disambiguation_level(const Position& pos, Move m, Notation n) { { Square s = pop_lsb(&b); if ( pos.pseudo_legal(make_move(s, to)) - && pos.legal(make_move(s, to)) - && !(is_shogi(n) && pos.unpromoted_piece_on(s) != pos.unpromoted_piece_on(from))) + && pos.legal(make_move(s, to)) + && !(is_shogi(n) && pos.unpromoted_piece_on(s) != pos.unpromoted_piece_on(from))) others |= s; } @@ -278,9 +288,9 @@ bool hasInsufficientMaterial(Color c, const Position& pos) { // Other win rules if ( pos.captures_to_hand() - || pos.count_in_hand(c, ALL_PIECES) - || pos.extinction_value() != VALUE_NONE - || (pos.capture_the_flag_piece() && pos.count(c, pos.capture_the_flag_piece()))) + || pos.count_in_hand(c, ALL_PIECES) + || pos.extinction_value() != VALUE_NONE + || (pos.capture_the_flag_piece() && pos.count(c, pos.capture_the_flag_piece()))) return false; // Restricted pieces @@ -308,3 +318,512 @@ bool hasInsufficientMaterial(Color c, const Position& pos) { return true; } + +namespace fen { + +enum FenValidation : int { + FEN_MISSING_SPACE_DELIM = -12, + FEN_INVALID_NB_PARTS = -11, + FEN_INVALID_CHAR = -10, + FEN_TOUCHING_KINGS = -9, + FEN_INVALID_BOARD_GEOMETRY = -8, + FEN_INVALID_POCKET_INFO = -7, + FEN_INVALID_SIDE_TO_MOVE = -6, + FEN_INVALID_CASTLING_INFO = -5, + FEN_INVALID_EN_PASSANT_SQ = -4, + FEN_INVALID_NUMBER_OF_KINGS = -3, + FEN_INVALID_HALF_MOVE_COUNTER = -2, + FEN_INVALID_MOVE_COUNTER = -1, + FEN_EMPTY = 0, + FEN_OK = 1 +}; +enum Validation : int { + NOK, + OK +}; + +struct CharSquare { + int rowIdx; + int fileIdx; + CharSquare() : rowIdx(-1), fileIdx(-1) {} + CharSquare(int rowIdx, int fileIdx) : rowIdx(rowIdx), fileIdx(fileIdx) {} +}; + +bool operator==(const CharSquare& s1, const CharSquare& s2) { + return s1.rowIdx == s2.rowIdx && s1.fileIdx == s2.fileIdx; +} + +bool operator!=(const CharSquare& s1, const CharSquare& s2) { + return !(s1 == s2); +} + +int non_root_euclidian_distance(const CharSquare& s1, const CharSquare& s2) { + return pow(s1.rowIdx - s2.rowIdx, 2) + pow(s1.fileIdx - s2.fileIdx, 2); +} + +class CharBoard { +private: + int nbRanks; + int nbFiles; + std::vector board; // fill an array where the pieces are for later geometry checks +public: + CharBoard(int nbRanks, int nbFiles) : nbRanks(nbRanks), nbFiles(nbFiles) { + assert(nbFiles > 0 && nbRanks > 0); + board = std::vector(nbRanks * nbFiles, ' '); + } + void set_piece(int rankIdx, int fileIdx, char c) { + board[rankIdx * nbFiles + fileIdx] = c; + } + char get_piece(int rowIdx, int fileIdx) const { + return board[rowIdx * nbFiles + fileIdx]; + } + int get_nb_ranks() const { + return nbRanks; + } + int get_nb_files() const { + return nbFiles; + } + /// Returns the square of a given character + CharSquare get_square_for_piece(char piece) const { + CharSquare s; + for (int r = 0; r < nbRanks; ++r) { + for (int c = 0; c < nbFiles; ++c) { + if (get_piece(r, c) == piece) { + s.rowIdx = r; + s.fileIdx = c; + return s; + } + } + } + return s; + } + /// Returns all square positions for a given piece + std::vector get_squares_for_piece(char piece) const { + std::vector squares; + for (int r = 0; r < nbRanks; ++r) { + for (int c = 0; c < nbFiles; ++c) { + if (get_piece(r, c) == piece) { + squares.emplace_back(CharSquare(r, c)); + } + } + } + return squares; + } + /// Checks if a given character is on a given rank index + bool is_piece_on_rank(char piece, int rowIdx) const { + for (int f = 0; f < nbFiles; ++f) + if (get_piece(rowIdx, f) == piece) + return true; + return false; + } + friend std::ostream& operator<<(std::ostream& os, const CharBoard& board); +}; + +std::ostream& operator<<(std::ostream& os, const CharBoard& board) { + for (int r = 0; r < board.nbRanks; ++r) { + for (int c = 0; c < board.nbFiles; ++c) { + os << "[" << board.get_piece(r, c) << "] "; + } + os << std::endl; + } + return os; +} + +Validation check_for_valid_characters(const std::string& firstFenPart, const std::string& validSpecialCharacters, const Variant* v) { + for (char c : firstFenPart) { + if (!isdigit(c) && v->pieceToChar.find(c) == std::string::npos && validSpecialCharacters.find(c) == std::string::npos) { + std::cerr << "Invalid piece character: '" << c << "'." << std::endl; + return NOK; + } + } + return OK; +} + +std::vector get_fen_parts(const std::string& fullFen, char delim) { + std::vector fenParts; + std::string curPart; + std::stringstream ss(fullFen); + while (std::getline(ss, curPart, delim)) + fenParts.emplace_back(curPart); + return fenParts; +} + +/// fills the character board according to a given FEN string +Validation fill_char_board(CharBoard& board, const std::string& fenBoard, const std::string& validSpecialCharacters, const Variant* v) { + int rankIdx = 0; + int fileIdx = 0; + + char prevChar = '?'; + for (char c : fenBoard) { + if (c == ' ' || c == '[') + break; + if (isdigit(c)) { + fileIdx += c - '0'; + // if we have multiple digits attached we can add multiples of 9 to compute the resulting number (e.g. -> 21 = 2 + 2 * 9 + 1) + if (isdigit(prevChar)) + fileIdx += 9 * (prevChar - '0'); + } + else if (c == '/') { + ++rankIdx; + if (fileIdx != board.get_nb_files()) { + std::cerr << "curRankWidth != nbFiles: " << fileIdx << " != " << board.get_nb_files() << std::endl; + return NOK; + } + if (rankIdx == board.get_nb_ranks()) + break; + fileIdx = 0; + } + else if (validSpecialCharacters.find(c) == std::string::npos) { // normal piece + if (fileIdx == board.get_nb_files()) { + std::cerr << "File index: " << fileIdx << " for piece '" << c << "' exceeds maximum of allowed number of files: " << board.get_nb_files() << "." << std::endl; + return NOK; + } + board.set_piece(v->maxRank-rankIdx, fileIdx, c); // we mirror the rank index because the black pieces are given first in the FEN + ++fileIdx; + } + prevChar = c; + } + + if (v->pieceDrops) { // pockets can either be defined by [] or / + if (rankIdx+1 != board.get_nb_ranks() && rankIdx != board.get_nb_ranks()) { + std::cerr << "Invalid number of ranks. Expected: " << board.get_nb_ranks() << " Actual: " << rankIdx+1 << std::endl; + return NOK; + } + } + else { + if (rankIdx+1 != board.get_nb_ranks()) { + std::cerr << "Invalid number of ranks. Expected: " << board.get_nb_ranks() << " Actual: " << rankIdx+1 << std::endl; + return NOK; + } + } + return OK; +} + +Validation fill_castling_info_splitted(const std::string& castlingInfo, std::array& castlingInfoSplitted) { + for (char c : castlingInfo) { + if (c != '-') { + if (!isalpha(c)) { + std::cerr << "Invalid castling specification: '" << c << "'." << std::endl; + return NOK; + } + else if (isupper(c)) + castlingInfoSplitted[WHITE] += tolower(c); + else + castlingInfoSplitted[BLACK] += c; + } + } + return OK; +} + +std::string color_to_string(Color c) { + switch (c) { + case WHITE: + return "WHITE"; + case BLACK: + return "BLACK"; + case COLOR_NB: + return "COLOR_NB"; + default: + return "INVALID_COLOR"; + } +} + +Validation check_960_castling(const std::array& castlingInfoSplitted, const CharBoard& board, const std::array& kingPositionsStart) { + + for (Color color : {WHITE, BLACK}) { + for (char charPiece : {'K', 'R'}) { + if (castlingInfoSplitted[color].size() == 0) + continue; + const Rank rank = Rank(kingPositionsStart[color].rowIdx); + if (color == BLACK) + charPiece = tolower(charPiece); + if (!board.is_piece_on_rank(charPiece, rank)) { + std::cerr << "The " << color_to_string(color) << " king and rook must be on rank " << rank << " if castling is enabled for " << color_to_string(color) << "." << std::endl; + return NOK; + } + } + } + return OK; +} + +std::string castling_rights_to_string(CastlingRights castlingRights) { + switch (castlingRights) { + case KING_SIDE: + return "KING_SIDE"; + case QUEEN_SIDE: + return "QUEENS_SIDE"; + case WHITE_OO: + return "WHITE_OO"; + case WHITE_OOO: + return "WHITE_OOO"; + case BLACK_OO: + return "BLACK_OO"; + case BLACK_OOO: + return "BLACK_OOO"; + case WHITE_CASTLING: + return "WHITE_CASTLING"; + case BLACK_CASTLING: + return "BLACK_CASTLING"; + case ANY_CASTLING: + return "ANY_CASTLING"; + case CASTLING_RIGHT_NB: + return "CASTLING_RIGHT_NB"; + default: + return "INVALID_CASTLING_RIGHTS"; + } +} + +Validation check_touching_kings(const CharBoard& board, const std::array& kingPositions) { + if (non_root_euclidian_distance(kingPositions[WHITE], kingPositions[BLACK]) <= 2) { + std::cerr << "King pieces are next to each other." << std::endl; + std::cerr << board << std::endl; + return NOK; + } + return OK; +} + +Validation check_standard_castling(std::array& castlingInfoSplitted, const CharBoard& board, + const std::array& kingPositions, const std::array& kingPositionsStart, + const std::array, 2>& rookPositionsStart) { + + for (Color c : {WHITE, BLACK}) { + if (castlingInfoSplitted[c].size() == 0) + continue; + if (kingPositions[c] != kingPositionsStart[c]) { + std::cerr << "The " << color_to_string(c) << " KING has moved. Castling is no longer valid for " << color_to_string(c) << "." << std::endl; + return NOK; + } + + for (CastlingRights castling: {KING_SIDE, QUEEN_SIDE}) { + CharSquare rookStartingSquare = castling == QUEEN_SIDE ? rookPositionsStart[c][0] : rookPositionsStart[c][1]; + char targetChar = castling == QUEEN_SIDE ? 'q' : 'k'; + char rookChar = 'R'; // we don't use v->pieceToChar[ROOK]; here because in the newzealand_variant the ROOK is replaced by ROOKNI + if (c == BLACK) + rookChar = tolower(rookChar); + if (castlingInfoSplitted[c].find(targetChar) != std::string::npos) { + if (board.get_piece(rookStartingSquare.rowIdx, rookStartingSquare.fileIdx) != rookChar) { + std::cerr << "The " << color_to_string(c) << " ROOK on the "<< castling_rights_to_string(castling) << " has moved. " + << castling_rights_to_string(castling) << " castling is no longer valid for " << color_to_string(c) << "." << std::endl; + return NOK; + } + } + + } + } + return OK; +} + +Validation check_pocket_info(const std::string& fenBoard, int nbRanks, const Variant* v, std::array& pockets) { + + char stopChar; + int offset = 0; + if (std::count(fenBoard.begin(), fenBoard.end(), '/') == nbRanks) { + // look for last '/' + stopChar = '/'; + } + else { + // pocket is defined as [ and ] + stopChar = '['; + offset = 1; + if (*(fenBoard.end()-1) != ']') { + std::cerr << "Pocket specification does not end with ']'." << std::endl; + return NOK; + } + } + + // look for last '/' + for (auto it = fenBoard.rbegin()+offset; it != fenBoard.rend(); ++it) { + const char c = *it; + if (c == stopChar) + return OK; + if (c != '-') { + if (v->pieceToChar.find(c) == std::string::npos) { + std::cerr << "Invalid pocket piece: '" << c << "'." << std::endl; + return NOK; + } + else { + if (isupper(c)) + pockets[WHITE] += tolower(c); + else + pockets[BLACK] += c; + } + } + } + std::cerr << "Pocket piece closing character '" << stopChar << "' was not found." << std::endl; + return NOK; +} + +Validation check_number_of_kings(const std::string& fenBoard, const Variant* v) { + int nbWhiteKings = std::count(fenBoard.begin(), fenBoard.end(), toupper(v->pieceToChar[KING])); + int nbBlackKings = std::count(fenBoard.begin(), fenBoard.end(), tolower(v->pieceToChar[KING])); + + if (nbWhiteKings != 1) { + std::cerr << "Invalid number of white kings. Expected: 1. Given: " << nbWhiteKings << std::endl; + return NOK; + } + if (nbBlackKings != 1) { + std::cerr << "Invalid number of black kings. Expected: 1. Given: " << nbBlackKings << std::endl; + return NOK; + } + return OK; +} + +Validation check_en_passant_square(const std::string& enPassantInfo) { + const char firstChar = enPassantInfo[0]; + if (firstChar != '-') { + if (enPassantInfo.size() != 2) { + std::cerr << "Invalid en-passant square '" << enPassantInfo << "'. Expects 2 characters. Actual: " << enPassantInfo.size() << " character(s)." << std::endl; + return NOK; + } + if (isdigit(firstChar)) { + std::cerr << "Invalid en-passant square '" << enPassantInfo << "'. Expects 1st character to be a digit." << std::endl; + return NOK; + } + const char secondChar = enPassantInfo[1]; + if (!isdigit(secondChar)) { + std::cerr << "Invalid en-passant square '" << enPassantInfo << "'. Expects 2nd character to be a non-digit." << std::endl; + return NOK; + } + } + return OK; +} + +bool no_king_piece_in_pockets(const std::array& pockets) { + return pockets[WHITE].find('k') == std::string::npos && pockets[BLACK].find('k') == std::string::npos; +} + +FenValidation validate_fen(const std::string& fen, const Variant* v) { + + const std::string validSpecialCharacters = "/+~[]-"; + // 0) Layout + // check for empty fen + if (fen.size() == 0) { + std::cerr << "Fen is empty." << std::endl; + return FEN_EMPTY; + } + + // check for space + if (fen.find(' ') == std::string::npos) { + std::cerr << "Fen misses space as delimiter." << std::endl; + return FEN_MISSING_SPACE_DELIM; + } + + std::vector fenParts = get_fen_parts(fen, ' '); + std::vector starFenParts = get_fen_parts(v->startFen, ' '); + const unsigned int nbFenParts = starFenParts.size(); + + // check for number of parts + if (fenParts.size() != nbFenParts) { + std::cerr << "Invalid number of fen parts. Expected: " << nbFenParts << " Actual: " << fenParts.size() << std::endl; + return FEN_INVALID_NB_PARTS; + } + + // 1) Part + // check for valid characters + if (check_for_valid_characters(fenParts[0], validSpecialCharacters, v) == NOK) { + return FEN_INVALID_CHAR; + } + + // check for number of ranks + const int nbRanks = v->maxRank + 1; + // check for number of files + const int nbFiles = v->maxFile + 1; + CharBoard board(nbRanks, nbFiles); // create a 2D character board for later geometry checks + + if (fill_char_board(board, fenParts[0], validSpecialCharacters, v) == NOK) + return FEN_INVALID_BOARD_GEOMETRY; + + // check for pocket + std::array pockets; + if (v->pieceDrops) { + if (check_pocket_info(fenParts[0], nbRanks, v, pockets) == NOK) + return FEN_INVALID_POCKET_INFO; + } + + // check for number of kings (skip all extinction variants for this check (e.g. horde is a sepcial case where only one side has a royal king)) + if (v->pieceTypes.find(KING) != v->pieceTypes.end() && v->extinctionPieceTypes.size() == 0) { + // we have a royal king in this variant, ensure that each side has exactly one king + // (variants like giveaway use the COMMONER piece type instead) + if (check_number_of_kings(fenParts[0], v) == NOK) + return FEN_INVALID_NUMBER_OF_KINGS; + + // if kings are still in pockets skip this check (e.g. placement_variant) + if (no_king_piece_in_pockets(pockets)) { + // check if kings are touching + std::array kingPositions; + // check if kings are touching + kingPositions[WHITE] = board.get_square_for_piece(toupper(v->pieceToChar[KING])); + kingPositions[BLACK] = board.get_square_for_piece(tolower(v->pieceToChar[KING])); + if (check_touching_kings(board, kingPositions) == NOK) + return FEN_TOUCHING_KINGS; + + // 3) Part + // check castling rights + if (v->castling) { + std::array castlingInfoSplitted; + if (fill_castling_info_splitted(fenParts[2], castlingInfoSplitted) == NOK) + return FEN_INVALID_CASTLING_INFO; + + if (castlingInfoSplitted[WHITE].size() != 0 || castlingInfoSplitted[BLACK].size() != 0) { + + CharBoard startBoard(board.get_nb_ranks(), board.get_nb_files()); + fill_char_board(startBoard, v->startFen, validSpecialCharacters, v); + std::array kingPositionsStart; + kingPositionsStart[WHITE] = startBoard.get_square_for_piece(toupper(v->pieceToChar[KING])); + kingPositionsStart[BLACK] = startBoard.get_square_for_piece(tolower(v->pieceToChar[KING])); + + if (v->chess960) { + if (check_960_castling(castlingInfoSplitted, board, kingPositionsStart) == NOK) + return FEN_INVALID_CASTLING_INFO; + } + else { + std::array, 2> rookPositionsStart; + // we don't use v->pieceToChar[ROOK]; here because in the newzealand_variant the ROOK is replaced by ROOKNI + rookPositionsStart[WHITE] = startBoard.get_squares_for_piece('R'); + rookPositionsStart[BLACK] = startBoard.get_squares_for_piece('r'); + + if (check_standard_castling(castlingInfoSplitted, board, kingPositions, kingPositionsStart, rookPositionsStart) == NOK) + return FEN_INVALID_CASTLING_INFO; + } + } + + } + } + } + + // 2) Part + // check side to move char + if (fenParts[1][0] != 'w' && fenParts[1][0] != 'b') { + std::cerr << "Invalid side to move specification: '" << fenParts[1][0] << "'." << std::endl; + return FEN_INVALID_SIDE_TO_MOVE; + } + + // 4) Part + // check en-passant square + if (v->doubleStep && v->pieceTypes.find(PAWN) != v->pieceTypes.end()) { + if (check_en_passant_square(fenParts[3]) == NOK) + return FEN_INVALID_EN_PASSANT_SQ; + } + + // 5) Part + // checkCounting is skipped because if only one check is required to win it must not be part of the FEN (e.g. karouk_variant) + + // 6) Part + // check half move counter + for (char c : fenParts[nbFenParts-2]) + if (!isdigit(c)) { + std::cerr << "Invalid half move counter: '" << c << "'." << std::endl; + return FEN_INVALID_HALF_MOVE_COUNTER; + } + + // 7) Part + // check move counter + for (char c : fenParts[nbFenParts-1]) + if (!isdigit(c)) { + std::cerr << "Invalid move counter: '" << c << "'." << std::endl; + return FEN_INVALID_MOVE_COUNTER; + } + + return FEN_OK; +} +} diff --git a/src/ffishjs.cpp b/src/ffishjs.cpp index 5f8fe4e..a94dc9e 100644 --- a/src/ffishjs.cpp +++ b/src/ffishjs.cpp @@ -50,6 +50,19 @@ void initialize_stockfish() { Bitbases::init(); } +#define DELIM " " + +inline void save_pop_back(std::string& s) { + if (s.size() != 0) { + s.pop_back(); + } +} + +const Variant* get_variant(const std::string& uciVariant) { + if (uciVariant.size() == 0) + return variants.find("chess")->second; + return variants.find(uciVariant)->second; +} class Board { // note: we can't use references for strings here due to conversion to JavaScript @@ -81,30 +94,22 @@ public: } std::string legal_moves() { - std::string moves = ""; - bool first = true; + std::string moves; for (const ExtMove& move : MoveList(this->pos)) { - if (first) { - moves = UCI::move(this->pos, move); - first = false; - } - else - moves += " " + UCI::move(this->pos, move); + moves += UCI::move(this->pos, move); + moves += DELIM; } + save_pop_back(moves); return moves; } std::string legal_moves_san() { - std::string movesSan = ""; - bool first = true; + std::string movesSan; for (const ExtMove& move : MoveList(this->pos)) { - if (first) { - movesSan = move_to_san(this->pos, move, NOTATION_SAN); - first = false; - } - else - movesSan += " " + move_to_san(this->pos, move, NOTATION_SAN); + movesSan += move_to_san(this->pos, move, NOTATION_SAN); + movesSan += DELIM; } + save_pop_back(movesSan); return movesSan; } @@ -192,16 +197,20 @@ public: if (moveNumbers) { variationSan = std::to_string(fullmove_number()); if (pos.side_to_move() == WHITE) - variationSan += ". "; + variationSan += ". "; else - variationSan += "..."; + variationSan += "..."; } variationSan += move_to_san(this->pos, moves.back(), Notation(notation)); } else { - if (moveNumbers && pos.side_to_move() == WHITE) - variationSan += " " + std::to_string(fullmove_number()) + "."; - variationSan += " " + move_to_san(this->pos, moves.back(), Notation(notation)); + if (moveNumbers && pos.side_to_move() == WHITE) { + variationSan += DELIM; + variationSan += std::to_string(fullmove_number()); + variationSan += "."; + } + variationSan += DELIM; + variationSan += move_to_san(this->pos, moves.back(), Notation(notation)); } states->emplace_back(); pos.do_move(moves.back(), states->back()); @@ -247,34 +256,44 @@ public: } std::string move_stack() const { - if (moveStack.size() == 0) { - return ""; - } - std::string moves = UCI::move(pos, moveStack[0]); - for(auto it = std::begin(moveStack)+1; it != std::end(moveStack); ++it) { - moves += " " + UCI::move(pos, *it); + std::string moves; + for(auto it = std::begin(moveStack); it != std::end(moveStack); ++it) { + moves += UCI::move(pos, *it); + moves += DELIM; } + save_pop_back(moves); return moves; } void push_moves(std::string uciMoves) { - std::stringstream ss(uciMoves); - std::string uciMove; - while (std::getline(ss, uciMove, ' ')) { - push(uciMove); - } + std::stringstream ss(uciMoves); + std::string uciMove; + while (std::getline(ss, uciMove, ' ')) { + push(uciMove); + } } void push_san_moves(std::string sanMoves) { - return push_san_moves(sanMoves, NOTATION_SAN); + return push_san_moves(sanMoves, NOTATION_SAN); } void push_san_moves(std::string sanMoves, Notation notation) { - std::stringstream ss(sanMoves); - std::string sanMove; - while (std::getline(ss, sanMove, ' ')) { - push_san(sanMove, notation); + std::stringstream ss(sanMoves); + std::string sanMove; + while (std::getline(ss, sanMove, ' ')) + push_san(sanMove, notation); + } + + std::string pocket(bool color) { + const Color c = Color(!color); + std::string pocket; + for (PieceType pt = KING; pt >= PAWN; --pt) { + for (int i = 0; i < pos.count_in_hand(c, pt); ++i) { + // only create BLACK pieces in order to convert to lower case + pocket += std::string(1, pos.piece_to_char()[make_piece(BLACK, pt)]); } + } + return pocket; } // TODO: return board in ascii notation @@ -297,9 +316,7 @@ private: initialize_stockfish(); Board::sfInitialized = true; } - if (uciVariant == "") - uciVariant = "chess"; - this->v = variants.find(uciVariant)->second; + v = get_variant(uciVariant); this->resetStates(); if (fen == "") fen = v->startFen; @@ -311,181 +328,186 @@ private: bool Board::sfInitialized = false; namespace ffish { + // returns the version of the Fairy-Stockfish binary + std::string info() { + return engine_info(); + } -// returns the version of the Fairy-Stockfish binary -std::string info() { - return engine_info(); -} - -template -void set_option(std::string name, T value) { - Options[name] = value; - Board::sfInitialized = false; -} + template + void set_option(std::string name, T value) { + Options[name] = value; + Board::sfInitialized = false; + } -std::string available_variants() { - bool first = true; - std::string availableVariants = ""; + std::string available_variants() { + std::string availableVariants; for (std::string variant : variants.get_keys()) { - if (first) { - first = false; - availableVariants = variant; - } - else - availableVariants += " " + variant; + availableVariants += variant; + availableVariants += DELIM; } + save_pop_back(availableVariants); return availableVariants; -} + } -void load_variant_config(std::string variantInitContent) { + void load_variant_config(std::string variantInitContent) { std::stringstream ss(variantInitContent); if (!Board::sfInitialized) - initialize_stockfish(); + initialize_stockfish(); variants.parse_istream(ss); Options["UCI_Variant"].set_combo(variants.get_keys()); Board::sfInitialized = true; -} + } + + std::string starting_fen(std::string uciVariant) { + const Variant* v = get_variant(uciVariant); + return v->startFen; + } + + int validate_fen(std::string fen, std::string uciVariant) { + const Variant* v = get_variant(uciVariant); + return fen::validate_fen(fen, v); + } + + int validate_fen(std::string fen) { + return validate_fen(fen, "chess"); + } } class Game { - private: - std::unordered_map header; - std::unique_ptr board; - std::string variant = "chess"; - std::string fen = ""; // start pos - bool is960 = false; - bool parsedGame = false; - public: - std::string header_keys() { - std::string keys = ""; - bool first = true; - for (auto it = header.begin(); it != header.end(); ++it) { - if (first) { - keys = it->first; - first = false; - } - else - keys += " " + it->first; - } - return keys; - } +private: + std::unordered_map header; + std::unique_ptr board; + std::string variant = "chess"; + std::string fen = ""; // start pos + bool is960 = false; + bool parsedGame = false; +public: + std::string header_keys() { + std::string keys; + for (auto it = header.begin(); it != header.end(); ++it) { + keys += it->first; + keys += DELIM; + } + save_pop_back(keys); + return keys; + } - std::string headers(std::string item) { - auto it = header.find(item); - if (it == header.end()) - return ""; - return it->second; - } + std::string headers(std::string item) { + auto it = header.find(item); + if (it == header.end()) + return ""; + return it->second; + } - std::string mainline_moves() { - if (!parsedGame) - return ""; - return board->move_stack(); - } + std::string mainline_moves() { + if (!parsedGame) + return ""; + return board->move_stack(); + } - friend Game read_game_pgn(std::string); + friend Game read_game_pgn(std::string); }; Game read_game_pgn(std::string pgn) { - Game game; - size_t lineStart = 0; - bool headersParsed = false; - - while(true) { - size_t lineEnd = pgn.find('\n', lineStart); - - if (lineEnd == std::string::npos) - lineEnd = pgn.size(); - - if (!headersParsed && pgn[lineStart] == '[') { - // parse header - // look for item - size_t headerKeyStart = lineStart+1; - size_t headerKeyEnd = pgn.find(' ', lineStart); - size_t headerItemStart = headerKeyEnd+2; - size_t headerItemEnd = pgn.find(']', headerKeyEnd)-1; - - // put item into list - game.header[pgn.substr(headerKeyStart, headerKeyEnd-headerKeyStart)] = pgn.substr(headerItemStart, headerItemEnd-headerItemStart); - } - else { - if (!headersParsed) { - headersParsed = true; - auto it = game.header.find("Variant"); - if (it != game.header.end()) { - game.variant = it->second; - std::transform(game.variant.begin(), game.variant.end(), game.variant.begin(), - [](unsigned char c){ return std::tolower(c); }); - game.is960 = it->second.find("960") != std::string::npos; - } - - it = game.header.find("FEN"); - if (it != game.header.end()) - game.fen = it->second; - - game.board = std::make_unique(game.variant, game.fen, game.is960); - game.parsedGame = true; - } - - // game line - size_t curIdx = lineStart; - while (curIdx <= lineEnd) { - if (pgn[curIdx] == '*') - return game; - - while (pgn[curIdx] == '{') { - // skip comment - curIdx = pgn.find('}', curIdx); - if (curIdx == std::string::npos) { - std::cerr << "Missing '}' for move comment while reading pgn." << std::endl; - return game; - } - curIdx += 2; - } - while (pgn[curIdx] == '(') { - // skip comment - curIdx = pgn.find(')', curIdx); - if (curIdx == std::string::npos) { - std::cerr << "Missing ')' for move comment while reading pgn." << std::endl; - return game; - } - curIdx += 2; - } - - if (pgn[curIdx] >= '0' && pgn[curIdx] <= '9') { - // we are at a move number -> look for next point - curIdx = pgn.find('.', curIdx); - if (curIdx == std::string::npos) - break; - ++curIdx; - // increment if we're at a space - while (curIdx < pgn.size() && pgn[curIdx] == ' ') - ++curIdx; - // increment if we're at a point - while (curIdx < pgn.size() && pgn[curIdx] == '.') - ++curIdx; - } - // extract sanMove - size_t sanMoveEnd = std::min(pgn.find(' ', curIdx), lineEnd); - if (sanMoveEnd > curIdx) { - std::string sanMove = pgn.substr(curIdx, sanMoveEnd-curIdx); - // clean possible ? and ! from string - size_t annotationChar1 = sanMove.find('?'); - size_t annotationChar2 = sanMove.find('!'); - if (annotationChar1 != std::string::npos || annotationChar2 != std::string::npos) - sanMove = sanMove.substr(0, std::min(annotationChar1, annotationChar2)); - game.board->push_san(sanMove); - } - curIdx = sanMoveEnd+1; - } + Game game; + size_t lineStart = 0; + bool headersParsed = false; + + while(true) { + size_t lineEnd = pgn.find('\n', lineStart); + + if (lineEnd == std::string::npos) + lineEnd = pgn.size(); + + if (!headersParsed && pgn[lineStart] == '[') { + // parse header + // look for item + size_t headerKeyStart = lineStart+1; + size_t headerKeyEnd = pgn.find(' ', lineStart); + size_t headerItemStart = headerKeyEnd+2; + size_t headerItemEnd = pgn.find(']', headerKeyEnd)-1; + + // put item into list + game.header[pgn.substr(headerKeyStart, headerKeyEnd-headerKeyStart)] = pgn.substr(headerItemStart, headerItemEnd-headerItemStart); + } + else { + if (!headersParsed) { + headersParsed = true; + auto it = game.header.find("Variant"); + if (it != game.header.end()) { + game.variant = it->second; + std::transform(game.variant.begin(), game.variant.end(), game.variant.begin(), + [](unsigned char c){ return std::tolower(c); }); + game.is960 = it->second.find("960") != std::string::npos; } - lineStart = lineEnd+1; - if (lineStart >= pgn.size()) + it = game.header.find("FEN"); + if (it != game.header.end()) + game.fen = it->second; + + game.board = std::make_unique(game.variant, game.fen, game.is960); + game.parsedGame = true; + } + + // game line + size_t curIdx = lineStart; + while (curIdx <= lineEnd) { + if (pgn[curIdx] == '*') + return game; + + while (pgn[curIdx] == '{') { + // skip comment + curIdx = pgn.find('}', curIdx); + if (curIdx == std::string::npos) { + std::cerr << "Missing '}' for move comment while reading pgn." << std::endl; + return game; + } + curIdx += 2; + } + while (pgn[curIdx] == '(') { + // skip comment + curIdx = pgn.find(')', curIdx); + if (curIdx == std::string::npos) { + std::cerr << "Missing ')' for move comment while reading pgn." << std::endl; return game; + } + curIdx += 2; + } + + if (pgn[curIdx] >= '0' && pgn[curIdx] <= '9') { + // we are at a move number -> look for next point + curIdx = pgn.find('.', curIdx); + if (curIdx == std::string::npos) + break; + ++curIdx; + // increment if we're at a space + while (curIdx < pgn.size() && pgn[curIdx] == ' ') + ++curIdx; + // increment if we're at a point + while (curIdx < pgn.size() && pgn[curIdx] == '.') + ++curIdx; + } + // extract sanMove + size_t sanMoveEnd = std::min(pgn.find(' ', curIdx), lineEnd); + if (sanMoveEnd > curIdx) { + std::string sanMove = pgn.substr(curIdx, sanMoveEnd-curIdx); + // clean possible ? and ! from string + size_t annotationChar1 = sanMove.find('?'); + size_t annotationChar2 = sanMove.find('!'); + if (annotationChar1 != std::string::npos || annotationChar2 != std::string::npos) + sanMove = sanMove.substr(0, std::min(annotationChar1, annotationChar2)); + game.board->push_san(sanMove); + } + curIdx = sanMoveEnd+1; + } } + lineStart = lineEnd+1; + + if (lineStart >= pgn.size()) return game; + } + return game; } @@ -522,7 +544,8 @@ EMSCRIPTEN_BINDINGS(ffish_js) { .function("moveStack", &Board::move_stack) .function("pushMoves", &Board::push_moves) .function("pushSanMoves", select_overload(&Board::push_san_moves)) - .function("pushSanMoves", select_overload(&Board::push_san_moves)); + .function("pushSanMoves", select_overload(&Board::push_san_moves)) + .function("pocket", &Board::pocket); class_("Game") .function("headerKeys", &Game::header_keys) .function("headers", &Game::headers) @@ -544,6 +567,9 @@ EMSCRIPTEN_BINDINGS(ffish_js) { function("readGamePGN", &read_game_pgn); function("variants", &ffish::available_variants); function("loadVariantConfig", &ffish::load_variant_config); + function("startingFen", &ffish::starting_fen); + function("validateFen", select_overload(&ffish::validate_fen)); + function("validateFen", select_overload(&ffish::validate_fen)); // TODO: enable to string conversion method // .class_function("getStringFromInstance", &Board::get_string_from_instance); } diff --git a/src/variant.cpp b/src/variant.cpp index 55f671b..8b9a66d 100644 --- a/src/variant.cpp +++ b/src/variant.cpp @@ -609,6 +609,7 @@ namespace { v->startFen = "pppppppp/pppppppp/8/8/8/8/PPPPPPPP/PPPPPPPP w 0 1"; v->promotionPieceTypes = {}; v->firstRankDoubleSteps = false; + v->doubleStep = false; v->castling = false; v->stalemateValue = -VALUE_MATE; v->flagPiece = BREAKTHROUGH_PIECE; diff --git a/src/variants.ini b/src/variants.ini index ecaf9cd..ee7ecd6 100644 --- a/src/variants.ini +++ b/src/variants.ini @@ -230,11 +230,13 @@ mustCapture = true # Hybrid variant of makruk and crazyhouse [makrukhouse:makruk] +startFen = rnsmksnr/8/pppppppp/8/8/PPPPPPPP/8/RNSKMSNR[] w - - 0 1 pieceDrops = true capturesToHand = true # Hybrid variant of xiangqi and crazyhouse [xiangqihouse:xiangqi] +startFen = rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR[] w - - 0 1 pieceDrops = true capturesToHand = true dropChecks = false @@ -243,6 +245,7 @@ blackDropRegion = *6 *7 *8 *9 *10 # Hybrid variant of janggi and crazyhouse [janggihouse:janggi] +startFen = rnba1abnr/4k4/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/4K4/RNBA1ABNR[] w - - 0 1 pieceDrops = true capturesToHand = true @@ -361,7 +364,7 @@ capturesToHand = true variantTemplate = shogi pieceToCharTable = PNBR.F.....++++.+Kpnbr.f.....++++.+k pocketSize = 8 -startFen = rnb+fkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNB+FKBNR w KQkq - 0 1 +startFen = rnb+fkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNB+FKBNR[] w KQkq - 0 1 commoner = c centaur = g archbishop = a diff --git a/tests/js/README.md b/tests/js/README.md index 6a7ed3e..79d058e 100644 --- a/tests/js/README.md +++ b/tests/js/README.md @@ -73,7 +73,8 @@ shatranj shogi shouse sittuyin suicide supply threekings xiangqi ## Custom variants Fairy-Stockfish also allows defining custom variants by loading a configuration file. -See e.g. the confiugration for connect4, tictactoe or janggihouse in [variants.ini](https://github.com/ianfab/Fairy-Stockfish/blob/master/src/variants.ini). + +See e.g. the configuration for **connect4**, **tictactoe** or **janggihouse** in [variants.ini](https://github.com/ianfab/Fairy-Stockfish/blob/master/src/variants.ini). ```javascript fs = require('fs'); let configFilePath = './variants.ini'; @@ -98,9 +99,15 @@ ffish['onRuntimeInitialized'] = () => { } ``` -Set a custom fen position: +Set a custom fen position including fen valdiation: ```javascript -board.setFen("rnb1kbnr/ppp1pppp/8/3q4/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 3"); +fen = "rnb1kbnr/ppp1pppp/8/3q4/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 3"; +if (ffish.valdiateFen(fen) == 1) { // ffish.valdiateFen(fen) can return different error codes, it returns 1 for FEN_OK + board.setFen(fen); +} +else { + console.error(`Fen couldn't be parsed.`); +} ``` Alternatively, you can initialize a board with a custom FEN directly: @@ -119,11 +126,11 @@ let legalMoves = board.legalMoves().split(" "); let legalMovesSan = board.legalMovesSan().split(" "); for (var i = 0; i < legalMovesSan.length; i++) { - console.log(`${i}: ${legalMoves[i]}, ${legalMoves}) + console.log(`${i}: ${legalMoves[i]}, ${legalMovesSan[i]}`) } ``` -Unfortunately, it is impossible for Emscripten to call the destructors on C++ object. +Unfortunately, it is impossible for Emscripten to call the destructor on C++ objects. Therefore, you need to call `.delete()` to free the heap memory of an object. ```javascript board.delete(); @@ -147,7 +154,10 @@ fs.readFile(pgnFilePath, 'utf8', function (err,data) { console.log(game.mainlineMoves()) let board = new ffish.Board(game.headers("Variant").toLowerCase()); - board.pushMoves(game.mainlineMoves()); + for (let idx = 0; idx < mainlineMoves.length; ++idx) { + board.push(mainlineMoves[idx]); + } + // or use board.pushMoves(game.mainlineMoves()); to push all moves at once let finalFen = board.fen(); board.delete(); @@ -183,8 +193,8 @@ cd Fairy-Stockfish/src ``` ```bash emcc -O3 --bind -DLARGEBOARDS -DPRECOMPUTED_MAGICS -DNNUE_EMBEDDING_OFF -DNO_THREADS \ - -s TOTAL_MEMORY=67108864 -s ALLOW_MEMORY_GROWTH=1 -s WASM_MEM_MAX=2147483648 \ - -s ASSERTIONS=0 -s SAFE_HEAP=0 \ + -s TOTAL_MEMORY=32MB -s ALLOW_MEMORY_GROWTH=1 -s WASM_MEM_MAX=1GB \ + -s ASSERTIONS=0 -s SAFE_HEAP=0 -std=c++17 -Wall \ -DNO_THREADS -DLARGEBOARDS -DPRECOMPUTED_MAGICS \ ffishjs.cpp \ benchmark.cpp \ @@ -226,8 +236,8 @@ cd Fairy-Stockfish/src ``` ```bash emcc -O3 --bind -DLARGEBOARDS -DPRECOMPUTED_MAGICS -DNNUE_EMBEDDING_OFF -DNO_THREADS \ - -s TOTAL_MEMORY=67108864 -s ALLOW_MEMORY_GROWTH=1 -s WASM_MEM_MAX=2147483648 \ - -s ASSERTIONS=0 -s SAFE_HEAP=0 \ + -s TOTAL_MEMORY=32MB -s ALLOW_MEMORY_GROWTH=1 -s WASM_MEM_MAX=1GB \ + -s ASSERTIONS=0 -s SAFE_HEAP=0 -std=c++17 -Wall \ -s ENVIRONMENT='web,worker' -s EXPORT_ES6=1 -s MODULARIZE=1 -s USE_ES6_IMPORT_META=0 \ ffishjs.cpp \ benchmark.cpp \ diff --git a/tests/js/package.json b/tests/js/package.json index 12ea650..ae5bf84 100644 --- a/tests/js/package.json +++ b/tests/js/package.json @@ -1,6 +1,6 @@ { "name": "ffish", - "version": "0.4.2", + "version": "0.4.4", "description": "A high performance WebAssembly chess variant library based on Fairy-Stockfish", "main": "ffish.js", "scripts": { diff --git a/tests/js/test.js b/tests/js/test.js index ca6331c..749045e 100644 --- a/tests/js/test.js +++ b/tests/js/test.js @@ -4,6 +4,8 @@ before(() => { pgnDir = __dirname + '/../pgn/'; srcDir = __dirname + '/../../src/'; ffish = require('./ffish.js'); + WHITE = true; + BLACK = false; ffish['onRuntimeInitialized'] = () => { resolve(); } @@ -380,6 +382,19 @@ describe('board.pushSanMoves(sanMoves, notation)', function () { }); }); +describe('board.pocket(turn)', function () { + it("it returns the pocket for the given player as a string with no delimeter. All pieces are returned in lower case.", () => { + let board = new ffish.Board("crazyhouse", "rnb1kbnr/ppp1pppp/8/8/8/5q2/PPPP1PPP/RNBQKB1R/Pnp w KQkq - 0 4"); + chai.expect(board.pocket(WHITE)).to.equal("p"); + chai.expect(board.pocket(BLACK)).to.equal("np"); + board.delete(); + let board2 = new ffish.Board("crazyhouse", "rnb1kbnr/ppp1pppp/8/8/8/5q2/PPPP1PPP/RNBQKB1R[Pnp] w KQkq - 0 4"); + chai.expect(board2.pocket(WHITE)).to.equal("p"); + chai.expect(board2.pocket(BLACK)).to.equal("np"); + board2.delete(); + }); +}); + describe('ffish.info()', function () { it("it returns the version of the Fairy-Stockfish binary", () => { chai.expect(ffish.info()).to.be.a('string'); @@ -407,6 +422,52 @@ describe('ffish.setOptionBool(name, value)', function () { }); }); +describe('ffish.startingFen(uciVariant)', function () { + it("it returns the starting fen for the given uci-variant.", () => { + chai.expect(ffish.startingFen("chess")).to.equal("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); + }); +}); + +describe('ffish.validateFen(fen)', function () { + it("it validates a given chess fen and returns +1 if fen is valid. Otherwise an error code will be returned.", () => { + chai.expect(ffish.validateFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")).to.equal(1); + chai.expect(ffish.validateFen("6k1/R7/2p4p/2P2p1P/PPb2Bp1/2P1K1P1/5r2/8 b - - 4 39")).to.equal(1); + }); +}); + +describe('ffish.validateFen(fen, uciVariant)', function () { + it("it validates a given fen and returns +1 if fen is valid. Otherwise an error code will be returned.", () => { + // check if starting fens are valid for all variants + const variants = ffish.variants().split(" ") + for (let idx = 0; idx < variants.length; ++idx) { + const startFen = ffish.startingFen(variants[idx]); + chai.expect(ffish.validateFen(startFen, variants[idx])).to.equal(1); + } + // alternative pocket piece formulation + chai.expect(ffish.validateFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR/RB w KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(1); + chai.expect(ffish.validateFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR/ w KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(1); + + // error id checks + chai.expect(ffish.validateFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[]wKQkq-3+301", "3check-crazyhouse")).to.equal(-12); + chai.expect(ffish.validateFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(-11); + chai.expect(ffish.validateFen("rnbqkbnr/ppppXppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] w KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(-10); + chai.expect(ffish.validateFen("rnbqkbnr/pppppKpp/8/8/8/8/PPPPPPPP/RNBQ1BNR[] w KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(-9); + chai.expect(ffish.validateFen("rnbqkbnr/ppppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] w KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(-8); + chai.expect(ffish.validateFen("rnbqkbnr/pppppppp/8/8/8/PPPPPPPP/RNBQKBNR[] w KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(-8); + chai.expect(ffish.validateFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[77] w KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(-7); + chai.expect(ffish.validateFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] o KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(-6); + chai.expect(ffish.validateFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] w K6kq - 3+3 0 1", "3check-crazyhouse")).to.equal(-5); + chai.expect(ffish.validateFen("rnbq1bnr/pppkpppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] w KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(-5); + chai.expect(ffish.validateFen("rnbqkbn1/pppppppr/8/8/8/8/PPPPPPPP/RNBQKBNR[] w KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(-5); + chai.expect(ffish.validateFen("rnbkqbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR/RB w KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(-5); + chai.expect(ffish.validateFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] w KQkq ss 3+3 0 1", "3check-crazyhouse")).to.equal(-4); + chai.expect(ffish.validateFen("rnbqkknr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] w KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(-3); + chai.expect(ffish.validateFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] w KQkq - 3+3 x 1", "3check-crazyhouse")).to.equal(-2); + chai.expect(ffish.validateFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] w KQkq - 3+3 0 -13", "3check-crazyhouse")).to.equal(-1); + chai.expect(ffish.validateFen("", "chess")).to.equal(0); + }); +}); + describe('ffish.readGamePGN(pgn)', function () { it("it reads a pgn string and returns a game object", () => { fs = require('fs'); @@ -424,8 +485,13 @@ describe('ffish.readGamePGN(pgn)', function () { } let game = ffish.readGamePGN(data); - let board = new ffish.Board(game.headers("Variant").toLowerCase()); - board.pushMoves(game.mainlineMoves()); + const variant = game.headers("Variant").toLowerCase(); + let board = new ffish.Board(variant); + const mainlineMoves = game.mainlineMoves().split(" "); + for (let idx2 = 0; idx2 < mainlineMoves.length; ++idx2) { + board.push(mainlineMoves[idx2]); + chai.expect(ffish.validateFen(board.fen(), variant)).to.equal(1); + } chai.expect(board.fen()).to.equal(expectedFens[idx]); board.delete(); game.delete(); -- 1.7.0.4