Refactor FEN validation
authorFabian Fichter <ianfab@users.noreply.github.com>
Fri, 9 Apr 2021 15:52:57 +0000 (17:52 +0200)
committerFabian Fichter <ianfab@users.noreply.github.com>
Sat, 10 Apr 2021 16:20:03 +0000 (18:20 +0200)
- Support X-FEN validation
- Support fetching variant list in pyffish
- Use variant configuration properties more consistently
- Fix a few corner cases
- Improve test coverage
- More consistent formatting

Closes #87.

src/apiutil.h
src/ffishjs.cpp
src/position.cpp
src/pyffish.cpp
src/variant.cpp
test.py
tests/js/test.js

index b452664..4472523 100644 (file)
 #include "variant.h"
 
 
-namespace PSQT {
-void init(const Variant* v);
-}
-
 enum Notation {
     NOTATION_DEFAULT,
     // https://en.wikipedia.org/wiki/Algebraic_notation_(chess)
@@ -51,6 +47,8 @@ Notation default_notation(const Variant* v) {
     return NOTATION_SAN;
 }
 
+namespace SAN {
+
 enum Disambiguation {
     NO_DISAMBIGUATION,
     FILE_DISAMBIGUATION,
@@ -86,7 +84,8 @@ std::string piece(const Position& pos, Move m, Notation n) {
 }
 
 std::string file(const Position& pos, Square s, Notation n) {
-    switch (n) {
+    switch (n)
+    {
     case NOTATION_SHOGI_HOSKING:
     case NOTATION_SHOGI_HODGES:
     case NOTATION_SHOGI_HODGES_NUMBER:
@@ -101,7 +100,8 @@ std::string file(const Position& pos, Square s, Notation n) {
 }
 
 std::string rank(const Position& pos, Square s, Notation n) {
-    switch (n) {
+    switch (n)
+    {
     case NOTATION_SHOGI_HOSKING:
     case NOTATION_SHOGI_HODGES_NUMBER:
         return std::to_string(pos.max_rank() - rank_of(s) + 1);
@@ -124,7 +124,8 @@ std::string rank(const Position& pos, Square s, Notation n) {
 }
 
 std::string square(const Position& pos, Square s, Notation n) {
-    switch (n) {
+    switch (n)
+    {
     case NOTATION_JANGGI:
         return rank(pos, s, n) + file(pos, s, n);
     default:
@@ -284,13 +285,15 @@ const std::string move_to_san(Position& pos, Move m, Notation n) {
     return san;
 }
 
-bool hasInsufficientMaterial(Color c, const Position& pos) {
+} // namespace SAN
+
+bool has_insufficient_material(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
@@ -309,20 +312,21 @@ bool hasInsufficientMaterial(Color c, const Position& pos) {
     for (PieceType pt : { BISHOP, FERS, FERS_ALFIL, ALFIL, ELEPHANT })
         colorbound |= pos.pieces(pt) & ~restricted;
     unbound = pos.pieces() ^ restricted ^ colorbound;
-    if ((colorbound & pos.pieces(c)) && (((DarkSquares & colorbound) && (~DarkSquares & colorbound)) || unbound))
+    if ((colorbound & pos.pieces(c)) && (((DarkSquares & colorbound) && (~DarkSquares & colorbound)) || unbound || pos.stalemate_value() != VALUE_DRAW || pos.check_counting()))
         return false;
 
     // Unbound pieces require one helper piece of either color
-    if ((pos.pieces(c) & unbound) && (popcount(pos.pieces() ^ restricted) >= 2 || pos.stalemate_value() != VALUE_DRAW))
+    if ((pos.pieces(c) & unbound) && (popcount(pos.pieces() ^ restricted) >= 2 || pos.stalemate_value() != VALUE_DRAW || pos.check_counting()))
         return false;
 
     return true;
 }
 
-namespace fen {
+namespace FEN {
 
 enum FenValidation : int {
-    FEN_MISSING_SPACE_DELIM = -12,
+    FEN_INVALID_COUNTING_RULE = -14,
+    FEN_INVALID_CHECK_COUNT = -13,
     FEN_INVALID_NB_PARTS = -11,
     FEN_INVALID_CHAR = -10,
     FEN_TOUCHING_KINGS = -9,
@@ -386,9 +390,12 @@ public:
     /// 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) {
+        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;
@@ -400,13 +407,10 @@ public:
     /// 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) {
+        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
@@ -420,18 +424,20 @@ public:
 };
 
 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) {
+    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) {
+    for (char c : firstFenPart)
+    {
+        if (!isdigit(c) && v->pieceToChar.find(c) == std::string::npos && v->pieceToCharSynonyms.find(c) == std::string::npos && validSpecialCharacters.find(c) == std::string::npos)
+        {
             std::cerr << "Invalid piece character: '" << c << "'." << std::endl;
             return NOK;
         }
@@ -454,18 +460,22 @@ Validation fill_char_board(CharBoard& board, const std::string& fenBoard, const
     int fileIdx = 0;
 
     char prevChar = '?';
-    for (char c : fenBoard) {
+    for (char c : fenBoard)
+    {
         if (c == ' ' || c == '[')
             break;
-        if (isdigit(c)) {
+        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 == '/') {
+        else if (c == '/')
+        {
             ++rankIdx;
-            if (fileIdx != board.get_nb_files()) {
+            if (fileIdx != board.get_nb_files())
+            {
                 std::cerr << "curRankWidth != nbFiles: " << fileIdx << " != " << board.get_nb_files() << std::endl;
                 return NOK;
             }
@@ -473,8 +483,10 @@ Validation fill_char_board(CharBoard& board, const std::string& fenBoard, const
                 break;
             fileIdx = 0;
         }
-        else if (validSpecialCharacters.find(c) == std::string::npos) {  // normal piece
-            if (fileIdx == board.get_nb_files()) {
+        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;
             }
@@ -484,14 +496,18 @@ Validation fill_char_board(CharBoard& board, const std::string& fenBoard, const
         prevChar = c;
     }
 
-    if (v->pieceDrops) { // pockets can either be defined by [] or /
-        if (rankIdx+1 != board.get_nb_ranks() && rankIdx != board.get_nb_ranks()) {
+    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()) {
+    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;
         }
@@ -499,10 +515,23 @@ Validation fill_char_board(CharBoard& board, const std::string& fenBoard, const
     return OK;
 }
 
+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 fill_castling_info_splitted(const std::string& castlingInfo, std::array<std::string, 2>& castlingInfoSplitted) {
-    for (char c : castlingInfo) {
-        if (c != '-') {
-            if (!isalpha(c)) {
+    for (char c : castlingInfo)
+    {
+        if (c != '-')
+        {
+            if (!isalpha(c))
+            {
                 std::cerr << "Invalid castling specification: '" << c << "'." << std::endl;
                 return NOK;
             }
@@ -516,7 +545,8 @@ Validation fill_castling_info_splitted(const std::string& castlingInfo, std::arr
 }
 
 std::string color_to_string(Color c) {
-    switch (c) {
+    switch (c)
+    {
     case WHITE:
         return "WHITE";
     case BLACK:
@@ -528,26 +558,9 @@ std::string color_to_string(Color c) {
     }
 }
 
-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) {
+    switch (castlingRights)
+    {
     case KING_SIDE:
         return "KING_SIDE";
     case QUEEN_SIDE:
@@ -573,35 +586,49 @@ std::string castling_rights_to_string(CastlingRights castlingRights) {
     }
 }
 
-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;
+Validation check_castling_rank(const std::array<std::string, 2>& castlingInfoSplitted, const CharBoard& board, const Variant* v) {
+
+    for (Color c : {WHITE, BLACK})
+    {
+        for (char charPiece : {v->pieceToChar[make_piece(c, v->castlingKingPiece)],
+                               v->pieceToChar[make_piece(c, v->castlingRookPiece)]})
+        {
+            if (castlingInfoSplitted[c].size() == 0)
+                continue;
+            const Rank castlingRank = relative_rank(c, v->castlingRank, v->maxRank);
+            if (!board.is_piece_on_rank(charPiece, castlingRank))
+            {
+                std::cerr << "The " << color_to_string(c) << " king and rook must be on rank " << castlingRank << " if castling is enabled for " << color_to_string(c) << "." << 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) {
+                             const std::array<std::vector<CharSquare>, 2>& rookPositionsStart, const Variant* v) {
 
-    for (Color c : {WHITE, BLACK}) {
+    for (Color c : {WHITE, BLACK})
+    {
         if (castlingInfoSplitted[c].size() == 0)
             continue;
-        if (kingPositions[c] != kingPositionsStart[c]) {
+        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}) {
+        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) {
+            char rookChar = v->pieceToChar[make_piece(c, v->castlingRookPiece)];
+            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;
@@ -613,132 +640,151 @@ Validation check_standard_castling(std::array<std::string, 2>& castlingInfoSplit
     return OK;
 }
 
-Validation check_pocket_info(const std::string& fenBoard, int nbRanks, const Variant* v, std::array<std::string, 2>& pockets) {
+Validation check_pocket_info(const std::string& fenBoard, int nbRanks, const Variant* v, std::string& pocket) {
 
     char stopChar;
     int offset = 0;
-    if (std::count(fenBoard.begin(), fenBoard.end(), '/') == nbRanks) {
+    if (std::count(fenBoard.begin(), fenBoard.end(), '/') == nbRanks)
+    {
         // look for last '/'
         stopChar = '/';
     }
-    else {
+    else
+    {
         // pocket is defined as [ and ]
         stopChar = '[';
         offset = 1;
-        if (*(fenBoard.end()-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) {
+    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) {
+        if (c != '-')
+        {
+            if (v->pieceToChar.find(c) == std::string::npos && v->pieceToCharSynonyms.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;
-            }
+            else
+                pocket += 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]));
+int piece_count(const std::string& fenBoard, Color c, PieceType pt, const Variant* v) {
+    return std::count(fenBoard.begin(), fenBoard.end(), v->pieceToChar[make_piece(c, pt)]);
+}
+
+Validation check_number_of_kings(const std::string& fenBoard, const std::string& startFenBoard, const Variant* v) {
+    int nbWhiteKings = piece_count(fenBoard, WHITE, KING, v);
+    int nbBlackKings = piece_count(fenBoard, BLACK, KING, v);
+    int nbWhiteKingsStart = piece_count(startFenBoard, WHITE, KING, v);
+    int nbBlackKingsStart = piece_count(startFenBoard, BLACK, KING, v);
 
-    if (nbWhiteKings != 1) {
-        std::cerr << "Invalid number of white kings. Expected: 1. Given: " << nbWhiteKings << std::endl;
+    if (nbWhiteKings != nbWhiteKingsStart)
+    {
+        std::cerr << "Invalid number of white kings. Expected: " << nbWhiteKingsStart << ". Given: " << nbWhiteKings << std::endl;
         return NOK;
     }
-    if (nbBlackKings != 1) {
-        std::cerr << "Invalid number of black kings. Expected: 1. Given: " << nbBlackKings << std::endl;
+    if (nbBlackKings != nbBlackKingsStart)
+    {
+        std::cerr << "Invalid number of black kings. Expected: " << nbBlackKingsStart << ". 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) {
+    if (enPassantInfo.size() != 1 || enPassantInfo[0] != '-')
+    {
+        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;
+        if (!isalpha(enPassantInfo[0]))
+        {
+            std::cerr << "Invalid en-passant square '" << enPassantInfo << "'. Expects 1st character to be a letter." << 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;
+        if (!isdigit(enPassantInfo[1]))
+        {
+            std::cerr << "Invalid en-passant square '" << enPassantInfo << "'. Expects 2nd character to be a 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;
+
+Validation check_check_count(const std::string& checkCountInfo) {
+    if (checkCountInfo.size() != 3)
+    {
+        std::cerr << "Invalid check count '" << checkCountInfo << "'. Expects 3 characters. Actual: " << checkCountInfo.size() << " character(s)." << std::endl;
+        return NOK;
+    }
+    if (!isdigit(checkCountInfo[0]))
+    {
+        std::cerr << "Invalid check count '" << checkCountInfo << "'. Expects 1st character to be a digit." << std::endl;
+        return NOK;
+    }
+    if (!isdigit(checkCountInfo[2])) {
+        std::cerr << "Invalid check count '" << checkCountInfo << "'. Expects 3rd character to be a digit." << std::endl;
+        return NOK;
+    }
+    return OK;
 }
 
-Validation check_digit_field(const std::string& field)
-{
-    if (field.size() == 1 && field[0] == '-') {
+
+Validation check_digit_field(const std::string& field) {
+    if (field.size() == 1 && field[0] == '-')
         return OK;
-    }
     for (char c : field)
-        if (!isdigit(c)) {
+        if (!isdigit(c))
             return NOK;
-        }
     return OK;
 }
 
 
-FenValidation validate_fen(const std::string& fen, const Variant* v) {
+FenValidation validate_fen(const std::string& fen, const Variant* v, bool chess960 = false) {
 
     const std::string validSpecialCharacters = "/+~[]-";
     // 0) Layout
     // check for empty fen
-    if (fen.size() == 0) {
+    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 (also up to two additional "-" for non-existing no-progress counter or castling rights)
-    const unsigned int maxNumberFenParts = 7U;
-    const unsigned int topThreshold = std::min(nbFenParts + 2, maxNumberFenParts);
-    if (fenParts.size() < nbFenParts || fenParts.size() > topThreshold) {
-        std::cerr << "Invalid number of fen parts. Expected: >= " << nbFenParts << " and <= " << topThreshold
+    // check for number of parts
+    const unsigned int maxNumberFenParts = 6 + v->checkCounting;
+    if (fenParts.size() < 1 || fenParts.size() > maxNumberFenParts)
+    {
+        std::cerr << "Invalid number of fen parts. Expected: >= 1 and <= " << maxNumberFenParts
                   << " 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) {
+    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;
@@ -750,94 +796,122 @@ FenValidation validate_fen(const std::string& fen, const Variant* v) {
         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)
+    std::string pocket = "";
+    if (v->pieceDrops || v->seirawanGating || v->arrowGating)
+    {
+        if (check_pocket_info(fenParts[0], nbRanks, v, pocket) == 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
+    // check for number of kings
+    if (v->pieceTypes.find(KING) != v->pieceTypes.end())
+    {
+        // we have a royal king in this variant,
+        // ensure that each side has exactly as many kings as in the starting position
         // (variants like giveaway use the COMMONER piece type instead)
-        if (check_number_of_kings(fenParts[0], v) == NOK)
+        if (check_number_of_kings(fenParts[0], starFenParts[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
+        // check for touching kings if there are exactly two royal kings on the board (excluding pocket)
+        if (   v->kingType == KING
+            && piece_count(fenParts[0], WHITE, KING, v) - piece_count(pocket, WHITE, KING, v) == 1
+            && piece_count(fenParts[0], BLACK, KING, v) - piece_count(pocket, BLACK, KING, v) == 1)
+        {
             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]));
+            kingPositions[WHITE] = board.get_square_for_piece(v->pieceToChar[make_piece(WHITE, KING)]);
+            kingPositions[BLACK] = board.get_square_for_piece(v->pieceToChar[make_piece(BLACK, 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') {
+    if (fenParts.size() >= 2 && 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;
     }
 
+    // Castling and en passant can be skipped
+    bool skipCastlingAndEp = fenParts.size() >= 4 && fenParts.size() <= 5 && isdigit(fenParts[2][0]);
+
+    // 3) Part
+    // check castling rights
+    if (fenParts.size() >= 3 && !skipCastlingAndEp && 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)
+        {
+            std::array<CharSquare, 2> kingPositions;
+            kingPositions[WHITE] = board.get_square_for_piece(toupper(v->pieceToChar[v->castlingKingPiece]));
+            kingPositions[BLACK] = board.get_square_for_piece(tolower(v->pieceToChar[v->castlingKingPiece]));
+
+            CharBoard startBoard(board.get_nb_ranks(), board.get_nb_files());
+            fill_char_board(startBoard, v->startFen, validSpecialCharacters, v);
+
+            // skip check for gating variants to avoid confusion with gating squares
+            if (!v->gating && check_castling_rank(castlingInfoSplitted, board, v) == NOK)
+                return FEN_INVALID_CASTLING_INFO;
+
+            // only check exact squares if starting position of castling pieces is known
+            if (!v->chess960 && !v->castlingDroppedPiece && !chess960)
+            {
+                std::array<CharSquare, 2> kingPositionsStart;
+                kingPositionsStart[WHITE] = startBoard.get_square_for_piece(v->pieceToChar[make_piece(WHITE, v->castlingKingPiece)]);
+                kingPositionsStart[BLACK] = startBoard.get_square_for_piece(v->pieceToChar[make_piece(BLACK, v->castlingKingPiece)]);
+                std::array<std::vector<CharSquare>, 2> rookPositionsStart;
+                rookPositionsStart[WHITE] = startBoard.get_squares_for_piece(v->pieceToChar[make_piece(WHITE, v->castlingRookPiece)]);
+                rookPositionsStart[BLACK] = startBoard.get_squares_for_piece(v->pieceToChar[make_piece(BLACK, v->castlingRookPiece)]);
+
+                if (check_standard_castling(castlingInfoSplitted, board, kingPositions, kingPositionsStart, rookPositionsStart, v) == NOK)
+                    return FEN_INVALID_CASTLING_INFO;
+            }
+        }
+    }
+
     // 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;
+    if (fenParts.size() >= 4 && !skipCastlingAndEp)
+    {
+        if (v->doubleStep && v->pieceTypes.find(PAWN) != v->pieceTypes.end())
+        {
+            if (check_en_passant_square(fenParts[3]) == NOK)
+                return FEN_INVALID_EN_PASSANT_SQ;
+        }
+        else if (v->countingRule && !check_digit_field(fenParts[3]))
+            return FEN_INVALID_COUNTING_RULE;
     }
 
     // 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)
+    // check check count
+    unsigned int optionalFields = 2 * !skipCastlingAndEp;
+    if (fenParts.size() >= 3 + optionalFields && v->checkCounting && fenParts.size() % 2)
+    {
+        if (check_check_count(fenParts[2 + optionalFields]) == NOK)
+            return FEN_INVALID_CHECK_COUNT;
+        optionalFields++;
+    }
 
     // 6) Part
     // check half move counter
-    if (!check_digit_field(fenParts[fenParts.size()-2])) {
+    if (fenParts.size() >= 3 + optionalFields && !check_digit_field(fenParts[fenParts.size()-2]))
+    {
         std::cerr << "Invalid half move counter: '" << fenParts[fenParts.size()-2] << "'." << std::endl;
         return FEN_INVALID_HALF_MOVE_COUNTER;
     }
 
     // 7) Part
     // check move counter
-    if (!check_digit_field(fenParts[fenParts.size()-1])) {
+    if (fenParts.size() >= 4 + optionalFields && !check_digit_field(fenParts[fenParts.size()-1]))
+    {
         std::cerr << "Invalid move counter: '" << fenParts[fenParts.size()-1] << "'." << std::endl;
         return FEN_INVALID_MOVE_COUNTER;
     }
 
     return FEN_OK;
 }
-}
+} // namespace FEN
index 59ff3b9..df29368 100644 (file)
@@ -117,7 +117,7 @@ public:
   std::string legal_moves_san() {
     std::string movesSan;
     for (const ExtMove& move : MoveList<LEGAL>(this->pos)) {
-      movesSan += move_to_san(this->pos, move, NOTATION_SAN);
+      movesSan += SAN::move_to_san(this->pos, move, NOTATION_SAN);
       movesSan += DELIM;
     }
     save_pop_back(movesSan);
@@ -145,7 +145,7 @@ public:
   bool push_san(std::string sanMove, Notation notation) {
     Move foundMove = MOVE_NONE;
     for (const ExtMove& move : MoveList<LEGAL>(pos)) {
-      if (sanMove == move_to_san(this->pos, move, notation)) {
+      if (sanMove == SAN::move_to_san(this->pos, move, notation)) {
         foundMove = move;
         break;
       }
@@ -180,7 +180,7 @@ public:
     pos.set(v, fen, is960, &states->back(), thread);
   }
 
-  // note: const identifier for pos not possible due to move_to_san()
+  // note: const identifier for pos not possible due to SAN::move_to_san()
   std::string san_move(std::string uciMove) {
     return san_move(uciMove, NOTATION_SAN);
   }
@@ -189,7 +189,7 @@ public:
     const Move move = UCI::to_move(this->pos, uciMove);
     if (is_move_none<true>(move, uciMove, pos))
       return "";
-    return move_to_san(this->pos, UCI::to_move(this->pos, uciMove), notation);
+    return SAN::move_to_san(this->pos, UCI::to_move(this->pos, uciMove), notation);
   }
 
   std::string variation_san(std::string uciMoves) {
@@ -222,7 +222,7 @@ public:
           else
           variationSan += "...";
         }
-        variationSan += move_to_san(this->pos, moves.back(), Notation(notation));
+        variationSan += SAN::move_to_san(this->pos, moves.back(), Notation(notation));
       }
       else {
         if (moveNumbers && pos.side_to_move() == WHITE) {
@@ -231,7 +231,7 @@ public:
           variationSan += ".";
         }
         variationSan += DELIM;
-        variationSan += move_to_san(this->pos, moves.back(), Notation(notation));
+        variationSan += SAN::move_to_san(this->pos, moves.back(), Notation(notation));
       }
       states->emplace_back();
       pos.do_move(moves.back(), states->back());
@@ -417,9 +417,13 @@ namespace ffish {
     return v->startFen;
   }
 
-  int validate_fen(std::string fen, std::string uciVariant) {
+  int validate_fen(std::string fen, std::string uciVariant, bool chess960) {
     const Variant* v = get_variant(uciVariant);
-    return fen::validate_fen(fen, v);
+    return FEN::validate_fen(fen, v, chess960);
+  }
+
+  int validate_fen(std::string fen, std::string uciVariant) {
+    return validate_fen(fen, uciVariant, false);
   }
 
   int validate_fen(std::string fen) {
@@ -655,6 +659,7 @@ EMSCRIPTEN_BINDINGS(ffish_js) {
   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));
+  function("validateFen", select_overload<int(std::string, std::string, bool)>(&ffish::validate_fen));
   // TODO: enable to string conversion method
   // .class_function("getStringFromInstance", &Board::get_string_from_instance);
 }
index cac4968..7b63851 100644 (file)
@@ -307,10 +307,7 @@ Position& Position::set(const Variant* v, const string& fenStr, bool isChess960,
 
   // 2. Active color
   ss >> token;
-  sideToMove = (token == 'w' || token == 'r' ? WHITE : BLACK);
-  // Invert side to move for SFEN
-  if (sfen)
-      sideToMove = ~sideToMove;
+  sideToMove = (token != (sfen ? 'w' : 'b') ? WHITE : BLACK);  // Invert colors for SFEN
   ss >> token;
 
   // 3-4. Skip parsing castling and en passant flags if not present
index e1ec295..0bc00dd 100644 (file)
@@ -60,6 +60,21 @@ extern "C" PyObject* pyffish_info(PyObject* self) {
     return Py_BuildValue("s", engine_info().c_str());
 }
 
+extern "C" PyObject* pyffish_variants(PyObject* self, PyObject *args) {
+    PyObject* varList = PyList_New(0);
+
+    for (std::string v : variants.get_keys())
+    {
+        PyObject* variant = Py_BuildValue("s", v.c_str());
+        PyList_Append(varList, variant);
+        Py_XDECREF(variant);
+    }
+
+    PyObject* Result = Py_BuildValue("O", varList);
+    Py_XDECREF(varList);
+    return Result;
+}
+
 // INPUT option name, option value
 extern "C" PyObject* pyffish_setOption(PyObject* self, PyObject *args) {
     const char *name;
@@ -131,7 +146,7 @@ extern "C" PyObject* pyffish_getSAN(PyObject* self, PyObject *args) {
     std::string moveStr = move;
 
     Py_XDECREF(moveList);
-    return Py_BuildValue("s", move_to_san(pos, UCI::to_move(pos, moveStr), notation).c_str());
+    return Py_BuildValue("s", SAN::move_to_san(pos, UCI::to_move(pos, moveStr), notation).c_str());
 }
 
 // INPUT variant, fen, movelist
@@ -159,7 +174,7 @@ extern "C" PyObject* pyffish_getSANmoves(PyObject* self, PyObject *args) {
         if ((m = UCI::to_move(pos, moveStr)) != MOVE_NONE)
         {
             //add to the san move list
-            PyObject *move = Py_BuildValue("s", move_to_san(pos, m, notation).c_str());
+            PyObject *move = Py_BuildValue("s", SAN::move_to_san(pos, m, notation).c_str());
             PyList_Append(sanMoves, move);
             Py_XDECREF(move);
 
@@ -309,8 +324,8 @@ extern "C" PyObject* pyffish_hasInsufficientMaterial(PyObject* self, PyObject *a
     StateListPtr states(new std::deque<StateInfo>(1));
     buildPosition(pos, states, variant, fen, moveList, chess960);
 
-    bool wInsufficient = hasInsufficientMaterial(WHITE, pos);
-    bool bInsufficient = hasInsufficientMaterial(BLACK, pos);
+    bool wInsufficient = has_insufficient_material(WHITE, pos);
+    bool bInsufficient = has_insufficient_material(BLACK, pos);
 
     return Py_BuildValue("(OO)", wInsufficient ? Py_True : Py_False, bInsufficient ? Py_True : Py_False);
 }
@@ -318,17 +333,19 @@ extern "C" PyObject* pyffish_hasInsufficientMaterial(PyObject* self, PyObject *a
 // INPUT variant, fen
 extern "C" PyObject* pyffish_validateFen(PyObject* self, PyObject *args) {
     const char *fen, *variant;
-    if (!PyArg_ParseTuple(args, "ss", &fen, &variant)) {
+    int chess960 = false;
+    if (!PyArg_ParseTuple(args, "ss|p", &fen, &variant, &chess960)) {
         return NULL;
     }
 
-    return Py_BuildValue("i", fen::validate_fen(std::string(fen), variants.find(std::string(variant))->second));
+    return Py_BuildValue("i", FEN::validate_fen(std::string(fen), variants.find(std::string(variant))->second, chess960));
 }
 
 
 static PyMethodDef PyFFishMethods[] = {
     {"version", (PyCFunction)pyffish_version, METH_NOARGS, "Get package version."},
     {"info", (PyCFunction)pyffish_info, METH_NOARGS, "Get Stockfish version info."},
+    {"variants", (PyCFunction)pyffish_variants, METH_NOARGS, "Get supported variants."},
     {"set_option", (PyCFunction)pyffish_setOption, METH_VARARGS, "Set UCI option."},
     {"load_variant_config", (PyCFunction)pyffish_loadVariantConfig, METH_VARARGS, "Load variant configuration."},
     {"start_fen", (PyCFunction)pyffish_startFen, METH_VARARGS, "Get starting position FEN."},
@@ -379,6 +396,9 @@ PyMODINIT_FUNC PyInit_pyffish() {
     PyModule_AddObject(module, "NOTATION_JANGGI", PyLong_FromLong(NOTATION_JANGGI));
     PyModule_AddObject(module, "NOTATION_XIANGQI_WXF", PyLong_FromLong(NOTATION_XIANGQI_WXF));
 
+    // validation
+    PyModule_AddObject(module, "FEN_OK", PyLong_FromLong(FEN::FEN_OK));
+
     // initialize stockfish
     pieceMap.init();
     variants.init();
index baf5baf..e41f6f5 100644 (file)
@@ -246,7 +246,7 @@ namespace {
         v->remove_piece(KNIGHT);
         v->startFen = "rmbqkbmr/pppppppp/8/8/8/8/PPPPPPPP/RMBQKBMR w KQkq - 0 1";
         v->kingType = KNIGHT;
-        v->castlingKingPiece = KNIGHT;
+        v->castlingKingPiece = KING;
         v->promotionPieceTypes = {COMMONER, QUEEN, ROOK, BISHOP};
         return v;
     }
diff --git a/test.py b/test.py
index 33c15e9..dae8d9b 100644 (file)
--- a/test.py
+++ b/test.py
@@ -18,7 +18,7 @@ SEIRAWAN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[HEhe] w KQBCDFGkqbcdfg -
 GRAND = "r8r/1nbqkcabn1/pppppppppp/10/10/10/10/PPPPPPPPPP/1NBQKCABN1/R8R w - - 0 1"
 GRANDHOUSE = "r8r/1nbqkcabn1/pppppppppp/10/10/10/10/PPPPPPPPPP/1NBQKCABN1/R8R[] w - - 0 1"
 XIANGQI = "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1"
-SHOGUN = "rnb+fkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNB+FKBNR w KQkq - 0 1"
+SHOGUN = "rnb+fkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNB+FKBNR[] w KQkq - 0 1"
 JANGGI = "rnba1abnr/4k4/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/4K4/RNBA1ABNR w - - 0 1"
 
 
@@ -31,7 +31,7 @@ capturesToHand = true
 
 # Shogun chess
 [shogun:crazyhouse]
-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
@@ -66,6 +66,10 @@ sf.load_variant_config(ini_text)
 
 variant_positions = {
     "chess": {
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1": (False, False),  # startpos
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -": (False, False),  # startpos
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR": (False, False),  # startpos
+        "rnbqkbnr/ppp2ppp/4p3/3pP3/8/8/PPPP1PPP/RNBQKBNR w KQkq d6 0 3": (False, False),
         "k7/8/8/8/8/8/8/K7 w - - 0 1": (True, True),  # K vs K
         "k7/b7/8/8/8/8/8/K7 w - - 0 1": (True, True),  # K vs KB
         "k7/n7/8/8/8/8/8/K7 w - - 0 1": (True, True),  # K vs KN
@@ -79,13 +83,41 @@ variant_positions = {
         "kb6/8/8/8/8/8/8/KB7 w - - 0 1": (False, False),  # KB vs KB opp color
         "8/8/8/8/8/6KN/8/6nk w - - 0 1": (False, False),  # KN vs KN
     },
+    "atomic": {
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1": (False, False),  # startpos
+    },
+    "3check": {
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 3+3 0 1": (False, False),  # startpos
+        "k7/n7/8/8/8/8/8/K7 w - - 1+2 0 1": (True, False),  # K vs KN
+        "k7/b7/8/8/8/8/8/K7 w - - 3+1 0 1": (True, False),  # K vs KB
+    },
+    "horde": {
+        "rnbqkbnr/pppppppp/8/1PP2PP1/PPPPPPPP/PPPPPPPP/PPPPPPPP/PPPPPPPP w kq - 0 1": (False, False),  # startpos
+    },
+    "racingkings": {
+        "8/8/8/8/8/8/krbnNBRK/qrbnNBRQ w - - 0 1": (False, False),  # startpos
+        "8/8/8/8/8/8/K6k/8 w - - 0 1": (False, False),  # KvK
+    },
+    "placement": {
+        "8/pppppppp/8/8/8/8/PPPPPPPP/8[KQRRBBNNkqrrbbnn] w - - 0 1": (False, False),  # startpos
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] w KQkq - 0 1": (False, False),  # chess startpos
+        "k7/8/8/8/8/8/8/K7[] w - - 0 1": (True, True),  # K vs K
+    },
+    "newzealand": {
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1": (False, False),  # startpos
+    },
     "seirawan": {
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[HEhe] w KQBCDFGkqbcdfg - 0 1": (False, False),  # startpos
         "k7/8/8/8/8/8/8/K7[] w - - 0 1": (True, True),  # K vs K
         "k7/8/8/8/8/8/8/KH6[] w - - 0 1": (False, True),  # KH vs K
         "k7/8/8/8/8/8/8/4K3[E] w E - 0 1": (False, True),  # KE vs K
     },
+    "cambodian": {
+        "rnsmksnr/8/pppppppp/8/8/PPPPPPPP/8/RNSKMSNR w DEde 0 0 1": (False, False),  # startpos
+        "1ns1ksn1/r6r/pppmpppp/3p4/8/PPPPPPPP/RK2N2R/1NS1MS2 w Ee - 6 5": (False, False),
+    },
     "sittuyin": {
-        "8/8/4pppp/pppp4/4PPPP/PPPP4/8/8[KFRRSSNNkfrrssnn] w - - 0 1": (False, False),  # starting position
+        "8/8/4pppp/pppp4/4PPPP/PPPP4/8/8[KFRRSSNNkfrrssnn] w - - 0 1": (False, False),  # startpos
         "k7/8/8/8/8/8/8/K7[] w - - 0 1": (True, True),  # K vs K
         "k6P/8/8/8/8/8/8/K7[] w - - 0 1": (True, True),  # KP vs K
         "k6P/8/8/8/8/8/8/K6p[] w - - 0 1": (False, False),  # KP vs KP
@@ -93,7 +125,7 @@ variant_positions = {
         "k7/8/8/8/8/8/8/KS6[] w - - 0 1": (False, True),  # KS vs K
     },
     "xiangqi": {
-        "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1": (False, False),  # starting position
+        "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1": (False, False),  # startpos
         "5k3/4a4/3CN4/9/1PP5p/9/8P/4C4/4A4/2B1K4 w - - 0 46": (False, False),  # issue #53
         "4k4/9/9/9/9/9/9/9/9/4K4 w - - 0 1": (True, True),  # K vs K
         "4k4/9/9/4p4/9/9/9/9/9/4KR3 w - - 0 1": (False, False),  # KR vs KP
@@ -102,7 +134,8 @@ variant_positions = {
         "4k4/9/9/9/9/9/9/9/4A4/4KC3 w - - 0 1": (False, True),  # KCA vs K
     },
     "janggi": {
-        JANGGI: (False, False),  # starting position
+        JANGGI: (False, False),  # startpos
+        "rhea1aehr/4k4/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/4K4/RHEA1AEHR w - - 0 1": (False, False),  # startpos
         "5k3/4a4/3CN4/9/1PP5p/9/8P/4C4/4A4/2B1K4 w - - 0 46": (False, False),  # issue #53
         "4k4/9/9/9/9/4B4/4B4/9/9/4K4 w - - 0 1": (False, True),  # KEE vs K
         "4k4/9/9/9/9/9/9/9/4A4/4KC3 w - - 0 1": (False, True),  # KCA vs K
@@ -120,6 +153,43 @@ variant_positions = {
     },
 }
 
+invalid_variant_positions = {
+    "chess": (
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 a",  # invalid full move
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - b 1",  # invalid half move
+        "rnbqkbnr/ppp2ppp/4p3/3pP3/8/8/PPPP1PPP/RNBQKBNR w KQkq -6 0 3",  # invalid en passant
+        "rnbqkbnr/ppp2ppp/4p3/3pP3/8/8/PPPP1PPP/RNBQKBNR w KQkq .6 0 3",  # invalid en passant
+        "rnbqkbnr/ppp2ppp/4p3/3pP3/8/8/PPPP1PPP/RNBQKBNR w KQkq d- 0 3",  # invalid en passant
+        "rnbqkbnr/ppp2ppp/4p3/3pP3/8/8/PPPP1PPP/RNBQKBNR w KQkq  0 3",  # invalid/missing en passant
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 123 - 0 1",  # invalid castling
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR g KQkq - 0 1",  # invalid side to move
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNH w KQkq - 0 1",  # invalid piece type
+        "rnbqkbnr/pppppppp/7/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",  # invalid file count
+        "rnbqkbnr/pppppppp/9/7/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",  # invalid file count
+        "rnbqkbnr/pppppppp/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",  # invalid rank count
+        "1nbqkbn1/pppppppp/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",  # missing castling rook
+        "rnbqkbnr/pppppppp/8/8/4K3/PPPPPPPP/RNBQ1BNR w KQkq - 0 1",  # king not on castling rank
+        "rnbqkbnr/pppppppp/8/8/RNBQKBNR/PPPPPPPP/8 w KQkq - 0 1",  # not on castling rank
+        "8/pppppppp/rnbqkbnr/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1",  # not on castling rank
+    ),
+    "atomic": (
+        "rnbqkbnr/pppppppp/8/8/8/RNBQKBNR/PPPPPPPP/8 w KQkq - 0 1",  # wrong castling rank
+    ),
+    "3check": (
+        "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 3+a 0 1",  # invalid check count
+    ),
+    "horde": (
+        "rnbqkbnr/pppppppp/8/1PP2PP1/PPPPPPPP/PPPPPPPP/PPPPPPPP/PPPPPPPK w kq - 0 1",  # wrong king count
+        "rnbq1bnr/pppppppp/8/1PP2PP1/PPPPPPPP/PPPPPPPP/PPPPPPPP/PPPPPPPP w kq - 0 1",  # wrong king count
+    ),
+    "sittuyin": (
+        "8/8/4pppp/pppp4/4PPPP/PPPP4/8/8[FRRSSNNkfrrssnn] w - - 0 1",  # wrong king count
+    ),
+    "shako": {
+        "c8c/ernbqkbnre/pppppppppp/10/10/10/10/PPPPPPPPPP/C8C/ERNBQKBNRE w KQkq - 0 1",  # not on castling rank
+    }
+}
+
 
 class TestPyffish(unittest.TestCase):
     def test_version(self):
@@ -130,6 +200,10 @@ class TestPyffish(unittest.TestCase):
         result = sf.info()
         self.assertTrue(result.startswith("Fairy-Stockfish"))
 
+    def test_variants_loaded(self):
+        variants = sf.variants()
+        self.assertTrue("shogun" in variants)
+
     def test_set_option(self):
         result = sf.set_option("UCI_Variant", "capablanca")
         self.assertIsNone(result)
@@ -195,6 +269,18 @@ class TestPyffish(unittest.TestCase):
         result = sf.get_fen("chess", CHESS, [])
         self.assertEqual(result, CHESS)
 
+        # incomplete FENs
+        result = sf.get_fen("chess", "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR", [])
+        self.assertEqual(result, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1")
+        result = sf.get_fen("chess", "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq -", [])
+        self.assertEqual(result, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1")
+        result = sf.get_fen("chess", "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w 1 2", [])
+        self.assertEqual(result, "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 1 2")
+
+        # alternative piece symbols
+        result = sf.get_fen("janggi", "rhea1aehr/4k4/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/4K4/RHEA1AEHR w - - 0 1", [])
+        self.assertEqual(result, JANGGI)
+
         result = sf.get_fen("capablanca", CAPA, [])
         self.assertEqual(result, CAPA)
 
@@ -525,10 +611,21 @@ class TestPyffish(unittest.TestCase):
                 self.assertEqual(result, expected_result, "{}: {}".format(variant, fen))
 
     def test_validate_fen(self):
+        # valid
         for variant, positions in variant_positions.items():
             for fen in positions:
-                self.assertTrue(sf.validate_fen(fen, variant) == 1, "{}: {}".format(variant, fen))
-
+                self.assertEqual(sf.validate_fen(fen, variant), sf.FEN_OK, "{}: {}".format(variant, fen))
+        # invalid
+        for variant, positions in invalid_variant_positions.items():
+            for fen in positions:
+                self.assertNotEqual(sf.validate_fen(fen, variant), sf.FEN_OK, "{}: {}".format(variant, fen))
+        # chess960
+        self.assertEqual(sf.validate_fen(CHESS960, "chess", True), sf.FEN_OK, "{}: {}".format(variant, fen))
+        self.assertEqual(sf.validate_fen("nrbqbkrn/pppppppp/8/8/8/8/PPPPPPPP/NRBQBKRN w BGbg - 0 1", "newzealand", True), sf.FEN_OK, "{}: {}".format(variant, fen))
+        # all variants starting positions
+        for variant in sf.variants():
+            fen = sf.start_fen(variant)
+            self.assertEqual(sf.validate_fen(fen, variant), sf.FEN_OK, "{}: {}".format(variant, fen))
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)
index 6478e34..44a7cdd 100644 (file)
@@ -567,8 +567,8 @@ describe('ffish.validateFen(fen, uciVariant)', function () {
       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/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[]wKQkq-3+301", "3check-crazyhouse")).to.equal(-10);
+      chai.expect(ffish.validateFen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] KQkq - 3+3 0 1", "3check-crazyhouse")).to.equal(-6);
       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);
@@ -587,6 +587,13 @@ describe('ffish.validateFen(fen, uciVariant)', function () {
     });
 });
 
+describe('ffish.validateFen(fen, uciVariant, chess960)', function () {
+  it("it validates a given X-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 AHah - 0 1", "chess", true)).to.equal(1);
+    chai.expect(ffish.validateFen("nrbqbkrn/pppppppp/8/8/8/8/PPPPPPPP/NRBQBKRN w BGbg - 0 1", "chess", true)).to.equal(1);
+  });
+});
+
 describe('ffish.readGamePGN(pgn)', function () {
   it("it reads a pgn string and returns a game object", () => {
      fs = require('fs');