From 061ad153e9167da50d7d677ceae008b57b0c4c49 Mon Sep 17 00:00:00 2001 From: Fabian Fichter Date: Fri, 9 Apr 2021 17:52:57 +0200 Subject: [PATCH] Refactor FEN validation - 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 | 436 +++++++++++++++++++++++++++++++---------------------- src/ffishjs.cpp | 21 ++- src/position.cpp | 5 +- src/pyffish.cpp | 32 ++++- src/variant.cpp | 2 +- test.py | 111 +++++++++++++- tests/js/test.js | 11 +- 7 files changed, 409 insertions(+), 209 deletions(-) diff --git a/src/apiutil.h b/src/apiutil.h index b452664..4472523 100644 --- a/src/apiutil.h +++ b/src/apiutil.h @@ -26,10 +26,6 @@ #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 get_squares_for_piece(char piece) const { std::vector squares; - for (int r = 0; r < nbRanks; ++r) { - for (int c = 0; c < nbFiles; ++c) { - if (get_piece(r, c) == piece) { + 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& 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& 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& castlingInfoSplitted, const CharBoard& board, const std::array& kingPositionsStart) { - - for (Color color : {WHITE, BLACK}) { - for (char charPiece : {'K', 'R'}) { - if (castlingInfoSplitted[color].size() == 0) - continue; - const Rank rank = Rank(kingPositionsStart[color].rowIdx); - if (color == BLACK) - charPiece = tolower(charPiece); - if (!board.is_piece_on_rank(charPiece, rank)) { - std::cerr << "The " << color_to_string(color) << " king and rook must be on rank " << rank << " if castling is enabled for " << color_to_string(color) << "." << std::endl; - return NOK; - } - } - } - return OK; -} - std::string castling_rights_to_string(CastlingRights castlingRights) { - switch (castlingRights) { + 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& 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& 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& castlingInfoSplitted, const CharBoard& board, const std::array& kingPositions, const std::array& kingPositionsStart, - const std::array, 2>& rookPositionsStart) { + const std::array, 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& castlingInfoSplit return OK; } -Validation check_pocket_info(const std::string& fenBoard, int nbRanks, const Variant* v, std::array& 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& 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 fenParts = get_fen_parts(fen, ' '); std::vector 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 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 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 castlingInfoSplitted; - if (fill_castling_info_splitted(fenParts[2], castlingInfoSplitted) == NOK) - return FEN_INVALID_CASTLING_INFO; - - if (castlingInfoSplitted[WHITE].size() != 0 || castlingInfoSplitted[BLACK].size() != 0) { - - CharBoard startBoard(board.get_nb_ranks(), board.get_nb_files()); - fill_char_board(startBoard, v->startFen, validSpecialCharacters, v); - std::array kingPositionsStart; - kingPositionsStart[WHITE] = startBoard.get_square_for_piece(toupper(v->pieceToChar[KING])); - kingPositionsStart[BLACK] = startBoard.get_square_for_piece(tolower(v->pieceToChar[KING])); - - if (v->chess960) { - if (check_960_castling(castlingInfoSplitted, board, kingPositionsStart) == NOK) - return FEN_INVALID_CASTLING_INFO; - } - else { - std::array, 2> rookPositionsStart; - // we don't use v->pieceToChar[ROOK]; here because in the newzealand_variant the ROOK is replaced by ROOKNI - rookPositionsStart[WHITE] = startBoard.get_squares_for_piece('R'); - rookPositionsStart[BLACK] = startBoard.get_squares_for_piece('r'); - - if (check_standard_castling(castlingInfoSplitted, board, kingPositions, kingPositionsStart, rookPositionsStart) == NOK) - return FEN_INVALID_CASTLING_INFO; - } - } - - } } } // 2) Part // check side to move char - if (fenParts[1][0] != 'w' && fenParts[1][0] != 'b') { + 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 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 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 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, 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 diff --git a/src/ffishjs.cpp b/src/ffishjs.cpp index 59ff3b9..df29368 100644 --- a/src/ffishjs.cpp +++ b/src/ffishjs.cpp @@ -117,7 +117,7 @@ public: std::string legal_moves_san() { std::string movesSan; for (const ExtMove& move : MoveList(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(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(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(&ffish::validate_fen)); function("validateFen", select_overload(&ffish::validate_fen)); + function("validateFen", select_overload(&ffish::validate_fen)); // TODO: enable to string conversion method // .class_function("getStringFromInstance", &Board::get_string_from_instance); } diff --git a/src/position.cpp b/src/position.cpp index cac4968..7b63851 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -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 diff --git a/src/pyffish.cpp b/src/pyffish.cpp index e1ec295..0bc00dd 100644 --- a/src/pyffish.cpp +++ b/src/pyffish.cpp @@ -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(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(); diff --git a/src/variant.cpp b/src/variant.cpp index baf5baf..e41f6f5 100644 --- a/src/variant.cpp +++ b/src/variant.cpp @@ -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 --- 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) diff --git a/tests/js/test.js b/tests/js/test.js index 6478e34..44a7cdd 100644 --- a/tests/js/test.js +++ b/tests/js/test.js @@ -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'); -- 1.7.0.4