Upgrade ffish.js to 0.4.4 and add validate_fen
authorJohannes Czech <QueensGambit@users.noreply.github.com>
Wed, 21 Oct 2020 22:51:47 +0000 (00:51 +0200)
committerGitHub <noreply@github.com>
Wed, 21 Oct 2020 22:51:47 +0000 (00:51 +0200)
* 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
src/ffishjs.cpp
src/variant.cpp
src/variants.ini
tests/js/README.md
tests/js/package.json
tests/js/test.js

index 1d185ea..8334eaf 100644 (file)
   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
+#include <vector>
+#include <string>
+#include <sstream>
+#include <cctype>
+#include <iostream>
+#include <math.h>
+
+#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<char> 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<char>(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<CharSquare> get_squares_for_piece(char piece) const {
+        std::vector<CharSquare> 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<std::string> get_fen_parts(const std::string& fullFen, char delim) {
+    std::vector<std::string> 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<std::string, 2>& 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<std::string, 2>& castlingInfoSplitted, const CharBoard& board, const std::array<CharSquare, 2>& 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<CharSquare, 2>& 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<std::string, 2>& castlingInfoSplitted, const CharBoard& board,
+                             const std::array<CharSquare, 2>& kingPositions, const std::array<CharSquare, 2>& kingPositionsStart,
+                             const std::array<std::vector<CharSquare>, 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<std::string, 2>& 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<std::string, 2>& 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<std::string> fenParts = get_fen_parts(fen, ' ');
+    std::vector<std::string> 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<std::string, 2> 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<CharSquare, 2> 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<std::string, 2> 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<CharSquare, 2> 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<std::vector<CharSquare>, 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;
+}
+}
index 5f8fe4e..a94dc9e 100644 (file)
@@ -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<LEGAL>(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<LEGAL>(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 <typename T>
-void set_option(std::string name, T value) {
-  Options[name] = value;
-  Board::sfInitialized = false;
-}
+  template <typename T>
+  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<false>(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<std::string, std::string> header;
-        std::unique_ptr<Board> 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<std::string, std::string> header;
+  std::unique_ptr<Board> 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<Board>(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<Board>(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<void(std::string)>(&Board::push_san_moves))
-    .function("pushSanMoves", select_overload<void(std::string, Notation)>(&Board::push_san_moves));
+    .function("pushSanMoves", select_overload<void(std::string, Notation)>(&Board::push_san_moves))
+    .function("pocket", &Board::pocket);
   class_<Game>("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<int(std::string)>(&ffish::validate_fen));
+  function("validateFen", select_overload<int(std::string, std::string)>(&ffish::validate_fen));
   // TODO: enable to string conversion method
   // .class_function("getStringFromInstance", &Board::get_string_from_instance);
 }
index 55f671b..8b9a66d 100644 (file)
@@ -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;
index ecaf9cd..ee7ecd6 100644 (file)
@@ -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
index 6a7ed3e..79d058e 100644 (file)
@@ -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 \
index 12ea650..ae5bf84 100644 (file)
@@ -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": {
index ca6331c..749045e 100644 (file)
@@ -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();