diff --git a/appveyor.yml b/appveyor.yml index c073aa73f..d6f5226e8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -70,7 +70,7 @@ build_script: $dummy = $nnuenet -match "(?nn-[a-z0-9]{12}.nnue)" $nnuenet = $Matches.nnuenet Write-Host "Default net:" $nnuenet - $nnuedownloadurl = "https://tests.stockfishchess.org/api/nn/$nnuenet" + $nnuedownloadurl = "https://github.com/official-stockfish/networks/raw/master/$nnuenet" $nnuefilepath = "src\${env:CONFIGURATION}\$nnuenet" if (Test-Path -Path $nnuefilepath) { Write-Host "Already available." diff --git a/setup.py b/setup.py index a03b8328e..fa72818d2 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ sources=sources, extra_compile_args=args) -setup(name="pyffish", version="0.0.78", +setup(name="pyffish", version="0.0.82", description="Fairy-Stockfish Python wrapper", long_description=long_description, long_description_content_type="text/markdown", diff --git a/src/Makefile b/src/Makefile index 602beefae..4fe40955b 100644 --- a/src/Makefile +++ b/src/Makefile @@ -539,7 +539,10 @@ ifeq ($(optimize),yes) endif ifeq ($(comp),clang) - CXXFLAGS += -fexperimental-new-pass-manager + clangmajorversion = $(shell $(CXX) -dumpversion 2>/dev/null | cut -f1 -d.) + ifeq ($(shell expr $(clangmajorversion) \< 16),1) + CXXFLAGS += -fexperimental-new-pass-manager + endif endif endif @@ -821,25 +824,35 @@ clean: objclean profileclean net: $(eval nnuenet := $(shell grep EvalFileDefaultName evaluate.h | grep define | sed 's/.*\(nn-[a-z0-9]\{12\}.nnue\).*/\1/')) @echo "Default net: $(nnuenet)" - $(eval nnuedownloadurl := https://tests.stockfishchess.org/api/nn/$(nnuenet)) + $(eval nnuedownloadurl1 := https://tests.stockfishchess.org/api/nn/$(nnuenet)) + $(eval nnuedownloadurl2 := https://github.com/official-stockfish/networks/raw/master/$(nnuenet)) $(eval curl_or_wget := $(shell if hash curl 2>/dev/null; then echo "curl -skL"; elif hash wget 2>/dev/null; then echo "wget -qO-"; fi)) - @if test -f "$(nnuenet)"; then \ - echo "Already available."; \ - else \ - if [ "x$(curl_or_wget)" = "x" ]; then \ - echo "Automatic download failed: neither curl nor wget is installed. Install one of these tools or download the net manually"; exit 1; \ - else \ - echo "Downloading $(nnuedownloadurl)"; $(curl_or_wget) $(nnuedownloadurl) > $(nnuenet);\ - fi; \ - fi; + @if [ "x$(curl_or_wget)" = "x" ]; then \ + echo "Automatic download failed: neither curl nor wget is installed. Install one of these tools or download the net manually"; exit 1; \ + fi $(eval shasum_command := $(shell if hash shasum 2>/dev/null; then echo "shasum -a 256 "; elif hash sha256sum 2>/dev/null; then echo "sha256sum "; fi)) - @if [ "x$(shasum_command)" != "x" ]; then \ - if [ "$(nnuenet)" != "nn-"`$(shasum_command) $(nnuenet) | cut -c1-12`".nnue" ]; then \ - echo "Failed download or $(nnuenet) corrupted, please delete!"; exit 1; \ - fi \ - else \ + @if [ "x$(shasum_command)" = "x" ]; then \ echo "shasum / sha256sum not found, skipping net validation"; \ fi + @for nnuedownloadurl in "$(nnuedownloadurl1)" "$(nnuedownloadurl2)"; do \ + if test -f "$(nnuenet)"; then \ + echo "$(nnuenet) available."; \ + else \ + if [ "x$(curl_or_wget)" != "x" ]; then \ + echo "Downloading $${nnuedownloadurl}"; $(curl_or_wget) $${nnuedownloadurl} > $(nnuenet);\ + fi; \ + fi; \ + if [ "x$(shasum_command)" != "x" ]; then \ + if [ "$(nnuenet)" != "nn-"`$(shasum_command) $(nnuenet) | cut -c1-12`".nnue" ]; then \ + echo "Removing failed download"; rm -f $(nnuenet); \ + else \ + echo "Network validated"; break; \ + fi; \ + fi; \ + done + @if ! test -f "$(nnuenet)"; then \ + echo "Failed to download $(nnuenet)."; \ + fi # clean binaries and objects objclean: diff --git a/src/apiutil.h b/src/apiutil.h index 0794abc34..d39102fcd 100644 --- a/src/apiutil.h +++ b/src/apiutil.h @@ -237,7 +237,7 @@ inline Disambiguation disambiguation_level(const Position& pos, Move m, Notation return SQUARE_DISAMBIGUATION; } - // A disambiguation occurs if we have more then one piece of type 'pt' + // A disambiguation occurs if we have more than one piece of type 'pt' // that can reach 'to' with a legal move. Bitboard b = pos.pieces(us, pt) ^ from; Bitboard others = 0; @@ -821,6 +821,16 @@ inline Validation check_number_of_kings(const std::string& fenBoard, const std:: 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. Maximum: 1. Given: " << nbWhiteKings << std::endl; + return NOK; + } + if (nbBlackKings > 1) + { + std::cerr << "Invalid number of black kings. Maximum: 1. Given: " << nbBlackKings << std::endl; + return NOK; + } if (nbWhiteKings != nbWhiteKingsStart) { std::cerr << "Invalid number of white kings. Expected: " << nbWhiteKingsStart << ". Given: " << nbWhiteKings << std::endl; diff --git a/src/bitboard.cpp b/src/bitboard.cpp index 3939296c3..4f0057597 100644 --- a/src/bitboard.cpp +++ b/src/bitboard.cpp @@ -111,7 +111,7 @@ namespace { const std::map GrasshopperDirectionsH { {EAST, 1}, {WEST, 1} }; const std::map GrasshopperDirectionsD { {NORTH_EAST, 1}, {SOUTH_EAST, 1}, {SOUTH_WEST, 1}, {NORTH_WEST, 1} }; - enum MovementType { RIDER, HOPPER, LAME_LEAPER, UNLIMITED_RIDER }; + enum MovementType { RIDER, HOPPER, LAME_LEAPER, HOPPER_RANGE }; template #ifdef PRECOMPUTED_MAGICS @@ -137,7 +137,9 @@ namespace { if (MT != HOPPER || hurdle) { attack |= s; - if (limit && MT != UNLIMITED_RIDER && ++count >= limit) + // For hoppers we consider limit == 1 as a grasshopper, + // but limit > 1 as a limited distance hopper + if (limit && !(MT == HOPPER_RANGE && limit == 1) && ++count >= limit) break; } @@ -300,7 +302,7 @@ void Bitboards::init_pieces() { leaper |= safe_destination(s, c == WHITE ? d : -d); } pseudo |= sliding_attack(pi->slider[initial][modality], s, 0, c); - pseudo |= sliding_attack(pi->hopper[initial][modality], s, 0, c); + pseudo |= sliding_attack(pi->hopper[initial][modality], s, 0, c); } } } @@ -420,7 +422,7 @@ namespace { // apply to the 64 or 32 bits word to get the index. Magic& m = magics[s]; // The mask for hoppers is unlimited distance, even if the hopper is limited distance (e.g., grasshopper) - m.mask = (MT == LAME_LEAPER ? lame_leaper_path(directions, s) : sliding_attack(directions, s, 0)) & ~edges; + m.mask = (MT == LAME_LEAPER ? lame_leaper_path(directions, s) : sliding_attack(directions, s, 0)) & ~edges; #ifdef LARGEBOARDS m.shift = 128 - popcount(m.mask); #else diff --git a/src/movegen.cpp b/src/movegen.cpp index 43807bb0b..f5a16eb41 100644 --- a/src/movegen.cpp +++ b/src/movegen.cpp @@ -29,7 +29,8 @@ namespace { ExtMove* make_move_and_gating(const Position& pos, ExtMove* moveList, Color us, Square from, Square to, PieceType pt = NO_PIECE_TYPE) { // Wall placing moves - if (pos.walling()) + //if it's "wall or move", and they chose non-null move, skip even generating wall move + if (pos.walling() && !(pos.variant()->wallOrMove && (from!=to))) { Bitboard b = pos.board_bb() & ~((pos.pieces() ^ from) | to); if (T == CASTLING) @@ -443,6 +444,12 @@ namespace { // Workaround for passing: Execute a non-move with any piece if (pos.pass(Us) && !pos.count(Us) && pos.pieces(Us)) *moveList++ = make(lsb(pos.pieces(Us)), lsb(pos.pieces(Us))); + + //if "wall or move", generate walling action with null move + if (pos.variant()->wallOrMove) + { + moveList = make_move_and_gating(pos, moveList, Us, lsb(pos.pieces(Us)), lsb(pos.pieces(Us))); + } } // King moves diff --git a/src/parser.cpp b/src/parser.cpp index 60196baf2..62dd6e0bd 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -472,6 +472,7 @@ Variant* VariantParser::parse(Variant* v) { parse_attribute("wallingRegionBlack", v->wallingRegion[BLACK]); parse_attribute("wallingRegion", v->wallingRegion[WHITE]); parse_attribute("wallingRegion", v->wallingRegion[BLACK]); + parse_attribute("wallOrMove", v->wallOrMove); parse_attribute("seirawanGating", v->seirawanGating); parse_attribute("commitGates", v->commitGates); parse_attribute("cambodianMoves", v->cambodianMoves); @@ -528,6 +529,7 @@ Variant* VariantParser::parse(Variant* v) { parse_attribute("flagPieceSafe", v->flagPieceSafe); parse_attribute("checkCounting", v->checkCounting); parse_attribute("connectN", v->connectN); + parse_attribute("connectPieceTypes", v->connectPieceTypes, v->pieceToChar); parse_attribute("connectHorizontal", v->connectHorizontal); parse_attribute("connectVertical", v->connectVertical); parse_attribute("connectDiagonal", v->connectDiagonal); @@ -536,15 +538,17 @@ Variant* VariantParser::parse(Variant* v) { parse_attribute("connectRegion1Black", v->connectRegion1[BLACK]); parse_attribute("connectRegion2Black", v->connectRegion2[BLACK]); parse_attribute("connectNxN", v->connectNxN); + parse_attribute("collinearN", v->collinearN); parse_attribute("connectValue", v->connectValue); parse_attribute("materialCounting", v->materialCounting); + parse_attribute("adjudicateFullBoard", v->adjudicateFullBoard); parse_attribute("countingRule", v->countingRule); parse_attribute("castlingWins", v->castlingWins); // Report invalid options if (DoCheck) { - const std::set& parsedKeys = config.get_comsumed_keys(); + const std::set& parsedKeys = config.get_consumed_keys(); for (const auto& it : config) if (parsedKeys.find(it.first) == parsedKeys.end()) std::cerr << "Invalid option: " << it.first << std::endl; diff --git a/src/parser.h b/src/parser.h index 36b208e06..04610b2ff 100644 --- a/src/parser.h +++ b/src/parser.h @@ -34,7 +34,7 @@ class Config : public std::map { consumedKeys.insert(s); return std::map::find(s); } - const std::set& get_comsumed_keys() { + const std::set& get_consumed_keys() { return consumedKeys; } private: diff --git a/src/position.cpp b/src/position.cpp index 043722ef8..0e798f96d 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -325,7 +325,7 @@ Position& Position::set(const Variant* v, const string& fenStr, bool isChess960, } // Promoted shogi pieces - else if (token == '+' && (idx = piece_to_char().find(ss.peek())) != string::npos) + else if (token == '+' && (idx = piece_to_char().find(ss.peek())) != string::npos && promoted_piece_type(type_of(Piece(idx)))) { ss >> token; put_piece(make_piece(color_of(Piece(idx)), promoted_piece_type(type_of(Piece(idx)))), sq, true, Piece(idx)); @@ -1343,7 +1343,8 @@ bool Position::pseudo_legal(const Move m) const { return checkers() ? MoveList< EVASIONS>(*this).contains(m) : MoveList(*this).contains(m); - if (walling()) + //if walling, and walling is not optional, or they didn't move, do the checks. + if (walling() && (!var->wallOrMove || (from==to))) { Bitboard wallsquares = st->wallSquares; @@ -2100,7 +2101,8 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { } // Add gated wall square - if (walling()) + // if wallOrMove, only actually place the wall if they gave up their move + if (walling() && (!var->wallOrMove || (from==to))) { // Reset wall squares for duck walling if (walling_rule() == DUCK) @@ -2771,7 +2773,7 @@ bool Position::is_optional_game_end(Value& result, int ply, int countStarted) co /// Position::is_immediate_game_end() tests whether the position ends the game /// immediately by a variant rule, i.e., there are no more legal moves. -/// It does not not detect stalemates. +/// It does not detect stalemates. bool Position::is_immediate_game_end(Value& result, int ply) const { @@ -2857,6 +2859,15 @@ bool Position::is_immediate_game_end(Value& result, int ply) const { result = mated_in(ply); return true; } + + //Calculate eligible pieces for connection once. + Bitboard connectPieces = 0; + for (PieceSet ps = connect_piece_types(); ps;){ + PieceType pt = pop_lsb(ps); + connectPieces |= pieces(pt); + }; + connectPieces &= pieces(~sideToMove); + // Connect-n if (connect_n() > 0) { @@ -2864,7 +2875,7 @@ bool Position::is_immediate_game_end(Value& result, int ply) const { for (Direction d : var->connect_directions) { - b = pieces(~sideToMove); + b = connectPieces; for (int i = 1; i < connect_n() && b; i++) b &= shift(d, b); if (b) @@ -2875,15 +2886,15 @@ bool Position::is_immediate_game_end(Value& result, int ply) const { } } - if ((var->connectRegion1[~sideToMove] & pieces(~sideToMove)) && (var->connectRegion2[~sideToMove] & pieces(~sideToMove))) + if ((var->connectRegion1[~sideToMove] & connectPieces) && (var->connectRegion2[~sideToMove] & connectPieces)) { Bitboard target = var->connectRegion2[~sideToMove]; - Bitboard current = var->connectRegion1[~sideToMove] & pieces(~sideToMove); + Bitboard current = var->connectRegion1[~sideToMove] & connectPieces; while (true) { Bitboard newBitboard = 0; for (Direction d : var->connect_directions) { - newBitboard |= shift(d, current | newBitboard) & pieces(~sideToMove); // the "| newBitboard" here probably saves a few loops + newBitboard |= shift(d, current | newBitboard) & connectPieces; // the "| newBitboard" here probably saves a few loops } if (newBitboard & target) { @@ -2903,7 +2914,7 @@ bool Position::is_immediate_game_end(Value& result, int ply) const { if (connect_nxn()) { - Bitboard connectors = pieces(~sideToMove); + Bitboard connectors = connectPieces; for (int i = 1; i < connect_nxn() && connectors; i++) connectors &= shift(connectors) & shift(connectors) & shift(connectors); if (connectors) @@ -2913,18 +2924,51 @@ bool Position::is_immediate_game_end(Value& result, int ply) const { } } - // Check for bikjang rule (Janggi) and double passing - if (st->pliesFromNull > 0 && ((st->bikjang && st->previous->bikjang) || (st->pass && st->previous->pass))) + // Collinear-n + if (collinear_n() > 0) { + Bitboard allPieces = connectPieces; + for (Direction d : var->connect_directions) { + Bitboard b = allPieces; + while (b) { + Square s = pop_lsb(b); + + int total_count = 1; // Start with the current piece + + // Check in both directions + for (int sign : {-1, 1}) { + Bitboard shifted = shift(sign * d, square_bb(s)); + while (shifted) { + if (shifted & b) { + total_count++; + b &= ~shifted; // Remove this piece from further consideration + } + shifted = shift(sign * d, shifted); + } + } + + if (total_count >= collinear_n()) { + result = convert_mate_value(-var->connectValue, ply); + return true; + } + } + } + } + + // Check for bikjang rule (Janggi), double passing, or board running full + if ( (st->pliesFromNull > 0 && ((st->bikjang && st->previous->bikjang) || (st->pass && st->previous->pass))) + || (var->adjudicateFullBoard && !(~pieces() & board_bb()))) { result = var->materialCounting ? convert_mate_value(material_counting_result(), ply) : VALUE_DRAW; return true; } + // Tsume mode: Assume that side with king wins when not in check if (tsumeMode && !count(~sideToMove) && count(sideToMove) && !checkers()) { result = mate_in(ply); return true; } + // Failing to checkmate with virtual pieces is a loss if (two_boards() && !checkers()) { diff --git a/src/position.h b/src/position.h index 721d231d1..f4cc7e94f 100644 --- a/src/position.h +++ b/src/position.h @@ -207,11 +207,13 @@ class Position { bool flag_reached(Color c) const; bool check_counting() const; int connect_n() const; + PieceSet connect_piece_types() const; bool connect_horizontal() const; bool connect_vertical() const; bool connect_diagonal() const; const std::vector& getConnectDirections() const; int connect_nxn() const; + int collinear_n() const; CheckCount checks_remaining(Color c) const; MaterialCounting material_counting() const; @@ -1047,6 +1049,11 @@ inline int Position::connect_n() const { return var->connectN; } +inline PieceSet Position::connect_piece_types() const { + assert(var != nullptr); + return var->connectPieceTypes; +} + inline bool Position::connect_horizontal() const { assert(var != nullptr); return var->connectHorizontal; @@ -1070,6 +1077,11 @@ inline int Position::connect_nxn() const { return var->connectNxN; } +inline int Position::collinear_n() const { + assert(var != nullptr); + return var->collinearN; +} + inline CheckCount Position::checks_remaining(Color c) const { return st->checksRemaining[c]; } diff --git a/src/pyffish.cpp b/src/pyffish.cpp index 537148c4d..fb8094f2f 100644 --- a/src/pyffish.cpp +++ b/src/pyffish.cpp @@ -54,7 +54,7 @@ void buildPosition(Position& pos, StateListPtr& states, const char *variant, con } extern "C" PyObject* pyffish_version(PyObject* self) { - return Py_BuildValue("(iii)", 0, 0, 78); + return Py_BuildValue("(iii)", 0, 0, 82); } extern "C" PyObject* pyffish_info(PyObject* self) { diff --git a/src/variant.cpp b/src/variant.cpp index 6e2e62fff..e7231dc8c 100644 --- a/src/variant.cpp +++ b/src/variant.cpp @@ -772,6 +772,8 @@ namespace { v->capturesToHand = false; v->whiteDropRegion = Rank1BB; v->blackDropRegion = Rank8BB; + v->promotionPieceTypes[WHITE] = piece_set(ARCHBISHOP) | QUEEN | ROOK | BISHOP | KNIGHT; + v->promotionPieceTypes[BLACK] = piece_set(ARCHBISHOP) | QUEEN | ROOK | BISHOP | KNIGHT; return v; } // Paradigm chess30 @@ -1169,6 +1171,7 @@ namespace { v->enclosingDrop = ATAXX; v->flipEnclosedPieces = ATAXX; v->materialCounting = UNWEIGHTED_MATERIAL; + v->adjudicateFullBoard = true; v->nMoveRule = 0; v->freeDrops = true; return v; @@ -1195,6 +1198,7 @@ namespace { v->enclosingDropStart = make_bitboard(SQ_D4, SQ_E4, SQ_D5, SQ_E5); v->flipEnclosedPieces = REVERSI; v->materialCounting = UNWEIGHTED_MATERIAL; + v->adjudicateFullBoard = true; return v; } // Flipello @@ -2091,6 +2095,17 @@ Variant* Variant::conclude() { connect_directions.push_back(SOUTH_EAST); } + // If not a connect variant, set connectPieceTypes to no pieces. + if ( !(connectRegion1[WHITE] || connectRegion1[BLACK] || connectN || connectNxN || collinearN) ) + { + connectPieceTypes = NO_PIECE_SET; + } + //Otherwise optimize to pieces actually in the game. + else + { + connectPieceTypes = connectPieceTypes & pieceTypes; + }; + return this; } diff --git a/src/variant.h b/src/variant.h index c83ac22cb..7dea254fd 100644 --- a/src/variant.h +++ b/src/variant.h @@ -108,6 +108,7 @@ struct Variant { bool gating = false; WallingRule wallingRule = NO_WALLING; Bitboard wallingRegion[COLOR_NB] = {AllSquares, AllSquares}; + bool wallOrMove = false; bool seirawanGating = false; bool commitGates = false; bool cambodianMoves = false; @@ -150,14 +151,17 @@ struct Variant { bool flagPieceSafe = false; bool checkCounting = false; int connectN = 0; + PieceSet connectPieceTypes = ~NO_PIECE_SET; bool connectHorizontal = true; bool connectVertical = true; bool connectDiagonal = true; Bitboard connectRegion1[COLOR_NB] = {}; Bitboard connectRegion2[COLOR_NB] = {}; int connectNxN = 0; + int collinearN = 0; Value connectValue = VALUE_MATE; MaterialCounting materialCounting = NO_MATERIAL_COUNTING; + bool adjudicateFullBoard = false; CountingRule countingRule = NO_COUNTING; CastlingRights castlingWins = NO_CASTLING; diff --git a/src/variants.ini b/src/variants.ini index ab00622ee..63430ac33 100644 --- a/src/variants.ini +++ b/src/variants.ini @@ -239,6 +239,7 @@ # wallingRule: rule on where wall can be placed [WallingRule] (default: none) # wallingRegionWhite: mask where wall squares (including duck) can be placed by white [Bitboard] (default: all squares) # wallingRegionBlack: mask where wall squares (including duck) can be placed by black [Bitboard] (default: all squares) +# wallOrMove: can wall or move, but not both [bool] (default: false) # seirawanGating: allow gating of pieces in hand like in S-Chess, requires "gating = true" [bool] (default: false) # commitGates: gating pieces are committed to gating squares like in Musketeer chess [bool] (default: false) # cambodianMoves: enable special moves of cambodian chess, requires "gating = true" [bool] (default: false) @@ -288,6 +289,7 @@ # flagPieceSafe: the flag piece must be safe to win [bool] (default: false) # checkCounting: enable check count win rule (check count is communicated via FEN, see 3check) [bool] (default: false) # connectN: number of aligned pieces for win [int] (default: 0) +# connectPieceTypes: pieces evaluated for connection rule [PieceSet] (default: *) # connectVertical: connectN looks at Vertical rows [bool] (default: true) # connectHorizontal: connectN looks at Horizontal rows [bool] (default: true) # connectDiagonal: connectN looks at Diagonal rows [bool] (default: true) @@ -296,8 +298,10 @@ # connectRegion1Black: " # connectRegion2Black: " # connectNxN: connect a tight NxN square for win [int] (default: 0) +# collinearN: arrange N pieces collinearly (other squares can be between pieces) [int] (default: 0) # connectValue: result in case of connect [Value] (default: win) # materialCounting: enable material counting rules [MaterialCounting] (default: none) +# adjudicateFullBoard: apply material counting immediately when board is full [bool] (default: false) # countingRule: enable counting rules [CountingRule] (default: none) # castlingWins: Specified castling moves are win conditions. Losing these rights is losing. [CastlingRights] (default: -) @@ -1553,7 +1557,7 @@ nMoveRule = 0 #https://ludii.games/details.php?keyword=Djara-Badakh #https://ludii.games/details.php?keyword=Tuk%20Tak customPiece1 = p:mKmNmAmD -#moves anywhere on the board, KNAD is an list of all possible moves on a 3x3 +#moves anywhere on the board, KNAD is a list of all possible moves on a 3x3 startFen = 3/3/3[PPPppp] w - - 0 1 mustDrop = true nMoveRule = 0 @@ -1667,9 +1671,9 @@ cannon = u customPiece1 = a:pR customPiece2 = c:mBcpB customPiece3 = i:pB -customPiece4 = w:mRpRFAcpR -customPiece5 = f:mBpBWDcpB -promotedPieceType = u:w a:w c:f i:f +customPiece4 = w:mRpRmFpB2 +customPiece5 = f:mBpBmWpR2 +promotedPieceType = u:w a:w c:f i:f p:g startFen = lnsgkgsnl/1rci1uab1/p1p1p1p1p/9/9/9/P1P1P1P1P/1BAU1ICR1/LNSGKGSNL[-] w 0 1 #https://www.chessvariants.com/difftaking.dir/deadsquare.html @@ -1816,9 +1820,7 @@ connectDiagonal = false #https://www.chessvariants.com/boardrules.dir/atlantis.html [atlantis:chess] wallingRule = edge -#not ready yet. Other wall variants are "move and wall", this is "move or wall". -#need to figure out way to do this ie. write code for: -#wallOrMove = true +wallOrMove = true #https://www.chessvariants.com/rules/ajax-orthodox-chess [ajax-orthodox:chess] @@ -1919,3 +1921,17 @@ enclosingDrop = anyside #http://gamescrafters.berkeley.edu/games.php?game=connect4 [cfour-misere:cfour] connectValue = loss + +#https://www.ludii.games/details.php?keyword=Three%20Musketeers +[three-musketeers] +pieceToCharTable = P......M..............p......m.............. +startFen = ppppM/ppppp/ppMpp/ppppp/Mpppp +maxRank = 5 +maxFile = 5 +collinearN = 3 +connectDiagonal = false +customPiece1 = m:cW +customPiece2 = p:mW +connectValue = loss +stalemateValue = win +connectPieceTypes = m diff --git a/test.py b/test.py index b2d953a5c..7a5aa456b 100644 --- a/test.py +++ b/test.py @@ -100,6 +100,19 @@ customPiece3 = c:hlN customPiece4 = d:hrN startFen = 7/7/7/3A3/7/7/7 w - - 0 1 + +[cannonshogi:shogi] +dropNoDoubled = - +shogiPawnDropMateIllegal = false +soldier = p +cannon = u +customPiece1 = a:pR +customPiece2 = c:mBcpB +customPiece3 = i:pB +customPiece4 = w:mRpRmFpB2 +customPiece5 = f:mBpBmWpR2 +promotedPieceType = u:w a:w c:f i:f +startFen = lnsgkgsnl/1rci1uab1/p1p1p1p1p/9/9/9/P1P1P1P1P/1BAU1ICR1/LNSGKGSNL[-] w 0 1 """ sf.load_variant_config(ini_text) @@ -317,6 +330,24 @@ def test_legal_moves(self): result = sf.legal_moves("shogun", SHOGUN, ["c2c4", "b8c6", "b2b4", "b7b5", "c4b5", "c6b8"]) self.assertIn("b5b6+", result) + # In Cannon Shogi the FGC and FSC can also move one square diagonally and, besides, + # move or capture two squares diagonally, by leaping an adjacent piece. + fen = "lnsg1gsnl/1rc1kuab1/p1+A1p1p1p/3P5/6i2/6P2/P1P1P3P/1B1U1ICR1/LNSGKGSNL[] w - - 1 3" + result = sf.legal_moves("cannonshogi", fen, []) + # mF + self.assertIn("c7b6", result) + self.assertIn("c7d8", result) + self.assertNotIn("c7d6", result) + self.assertNotIn("c7b8", result) + # pB2 + self.assertIn("c7a9", result) + self.assertIn("c7e5", result) + self.assertNotIn("c7a5", result) + self.assertNotIn("c7e9", result) + # verify distance limited to 2 + self.assertNotIn("c7f4", result) + self.assertNotIn("c7g3", result) + # Cambodian queen cannot capture with its leap # Cambodian king cannot leap to escape check result = sf.legal_moves("cambodian", CAMBODIAN, ["b1d2", "g8e7", "d2e4", "d6d5", "e4d6"]) @@ -977,177 +1008,127 @@ def test_game_result(self): result = sf.game_result("royalduck", "rnbqk1nr/pppp1ppp/4p3/8/7P/5Pb1/PPPPP*P1/RNBQKBNR w KQkq - 1 4", []) self.assertEqual(result, sf.VALUE_MATE) + def _check_immediate_game_end(self, variant, fen, moves, game_end, game_result=None): + with self.subTest(variant=variant, fen=fen, game_end=game_end, game_result=game_result): + result = sf.is_immediate_game_end(variant, fen, moves) + self.assertEqual(result[0], game_end) + if game_result is not None: + self.assertEqual(result[1], game_result) + def test_is_immediate_game_end(self): - result = sf.is_immediate_game_end("capablanca", CAPA, []) - self.assertFalse(result[0]) + self._check_immediate_game_end("capablanca", CAPA, [], False) # bikjang (facing kings) moves = "e2e3 e9f9 h3d3 e7f7 i1i3 h10i8 i3h3 c10e7 h3h8 i10i9 h8b8 i9g9 d3f3 f9e9 f3f10 e7c10 f10c10 b10c8 c10g10 g9f9 b8c8 a10b10 b3f3 f9h9 a1a2 h9f9 a2d2 b10b9 d2d10 e9d10 c8c10 d10d9 f3f9 i8g9 f9b9 a7a6 g10g7 f7f6 e4e5 c7d7 g1e4 i7i6 e4b6 d9d8 c10c8 d8d9 b9g9 d7d6 b6e8 i6h6 e5e6 f6e6 c1e4 a6b6 e4b6 d6d5 c4c5 d9d10 e3d3 h6i6 c5c6 d5c5" - result = sf.is_immediate_game_end("janggi", JANGGI, moves.split()) - self.assertFalse(result[0]) + self._check_immediate_game_end("janggi", JANGGI, moves.split(), False) moves = "e2e3 e9f9 h3d3 e7f7 i1i3 h10i8 i3h3 c10e7 h3h8 i10i9 h8b8 i9g9 d3f3 f9e9 f3f10 e7c10 f10c10 b10c8 c10g10 g9f9 b8c8 a10b10 b3f3 f9h9 a1a2 h9f9 a2d2 b10b9 d2d10 e9d10 c8c10 d10d9 f3f9 i8g9 f9b9 a7a6 g10g7 f7f6 e4e5 c7d7 g1e4 i7i6 e4b6 d9d8 c10c8 d8d9 b9g9 d7d6 b6e8 i6h6 e5e6 f6e6 c1e4 a6b6 e4b6 d6d5 c4c5 d9d10 e3d3 h6i6 c5c6 d5c5 d3d3" - result = sf.is_immediate_game_end("janggi", JANGGI, moves.split()) - self.assertTrue(result[0]) - self.assertEqual(result[1], -sf.VALUE_MATE) + self._check_immediate_game_end("janggi", JANGGI, moves.split(), True, -sf.VALUE_MATE) + + # full board adjudication + self._check_immediate_game_end("flipello", "pppppppp/pppppppp/pppPpppp/pPpPpppp/pppppppp/pPpPPPPP/ppPpPPpp/pppppppp[PPpp] b - - 63 32", [], True, sf.VALUE_MATE) + self._check_immediate_game_end("ataxx", "PPPpppp/pppPPPp/pPPPPPP/PPPPPPp/ppPPPpp/pPPPPpP/pPPPPPP b - - 99 50", [], True, -sf.VALUE_MATE) + self._check_immediate_game_end("ataxx", "PPPpppp/pppPPPp/pPP*PPP/PP*P*Pp/ppP*Ppp/pPPPPpP/pPPPPPP b - - 99 50", [], True, -sf.VALUE_MATE) + + def _check_optional_game_end(self, variant, fen, moves, game_end, game_result=None): + with self.subTest(variant=variant, fen=fen, game_end=game_end, game_result=game_result): + result = sf.is_optional_game_end(variant, fen, moves) + self.assertEqual(result[0], game_end) + if game_result is not None: + self.assertEqual(result[1], game_result) def test_is_optional_game_end(self): - result = sf.is_optional_game_end("capablanca", CAPA, []) - self.assertFalse(result[0]) + self._check_optional_game_end("capablanca", CAPA, [], False) # sittuyin stalemate due to optional promotion - result = sf.is_optional_game_end("sittuyin", "1k4PK/3r4/8/8/8/8/8/8[] w - - 0 1", []) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_DRAW) + self._check_optional_game_end("sittuyin", "1k4PK/3r4/8/8/8/8/8/8[] w - - 0 1", [], True, sf.VALUE_DRAW) # Xiangqi chasing rules # Also see http://www.asianxiangqi.org/English/AXF_rules_Eng.pdf # Direct chase by cannon - result = sf.is_optional_game_end("xiangqi", "2bakabnr/9/r1n1c4/2p1p1p1p/PP7/9/4P1P1P/2C3NC1/9/1NBAKAB1R w - - 0 1", ["c3a3", "a8b8", "a3b3", "b8a8", "b3a3", "a8b8", "a3b3", "b8a8", "b3a3"]) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "2bakabnr/9/r1n1c4/2p1p1p1p/PP7/9/4P1P1P/2C3NC1/9/1NBAKAB1R w - - 0 1", ["c3a3", "a8b8", "a3b3", "b8a8", "b3a3", "a8b8", "a3b3", "b8a8", "b3a3"], True, sf.VALUE_MATE) # Chase with chasing side to move - result = sf.is_optional_game_end("xiangqi", "2bakabnr/9/r1n1c4/2p1p1p1p/PP7/9/4P1P1P/2C3NC1/9/1NBAKAB1R w - - 0 1", ["c3a3", "a8b8", "a3b3", "b8a8", "b3a3", "a8b8", "a3b3", "b8a8", "b3a3", "a8b8", "a3b3", "b8a8"]) - self.assertTrue(result[0]) - self.assertEqual(result[1], -sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "2bakabnr/9/r1n1c4/2p1p1p1p/PP7/9/4P1P1P/2C3NC1/9/1NBAKAB1R w - - 0 1", ["c3a3", "a8b8", "a3b3", "b8a8", "b3a3", "a8b8", "a3b3", "b8a8", "b3a3", "a8b8", "a3b3", "b8a8"], True, -sf.VALUE_MATE) # Discovered chase by cannon (including pawn capture) - result = sf.is_optional_game_end("xiangqi", "2bakabr1/9/9/r1p1p1p2/p7R/P8/9/9/9/CC1AKA3 w - - 0 1", ["a5a6", "a7b7", "a6b6", "b7a7", "b6a6", "a7b7", "a6b6", "b7a7", "b6a6"]) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "2bakabr1/9/9/r1p1p1p2/p7R/P8/9/9/9/CC1AKA3 w - - 0 1", ["a5a6", "a7b7", "a6b6", "b7a7", "b6a6", "a7b7", "a6b6", "b7a7", "b6a6"], True, sf.VALUE_MATE) # Chase by soldier (draw) - result = sf.is_optional_game_end("xiangqi", "2bakabr1/9/9/r1p1p1p2/p7R/P8/9/9/9/1C1AKA3 w - - 0 1", ["a5a6", "a7b7", "a6b6", "b7a7", "b6a6", "a7b7", "a6b6", "b7a7", "b6a6"]) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_DRAW) + self._check_optional_game_end("xiangqi", "2bakabr1/9/9/r1p1p1p2/p7R/P8/9/9/9/1C1AKA3 w - - 0 1", ["a5a6", "a7b7", "a6b6", "b7a7", "b6a6", "a7b7", "a6b6", "b7a7", "b6a6"], True, sf.VALUE_DRAW) # Discovered and anti-discovered chase by cannon - result = sf.is_optional_game_end("xiangqi", "5k3/9/9/5C3/5c3/5C3/9/9/5p3/4K4 w - - 0 1", ["f5d5", "f6d6", "d5f5", "d6f6", "f5d5", "f6d6", "d5f5", "d6f6"]) - self.assertTrue(result[0]) - self.assertEqual(result[1], -sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "5k3/9/9/5C3/5c3/5C3/9/9/5p3/4K4 w - - 0 1", ["f5d5", "f6d6", "d5f5", "d6f6", "f5d5", "f6d6", "d5f5", "d6f6"], True, -sf.VALUE_MATE) # Mutual chase (draw) - result = sf.is_optional_game_end("xiangqi", "4k4/7n1/9/4pR3/9/9/4P4/9/9/4K4 w - - 0 1", ["f7h7"] + 2 * ["h9f8", "h7h8", "f8g6", "h8g8", "g6i7", "g8g7", "i7h9", "g7h7"]) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_DRAW) + self._check_optional_game_end("xiangqi", "4k4/7n1/9/4pR3/9/9/4P4/9/9/4K4 w - - 0 1", ["f7h7"] + 2 * ["h9f8", "h7h8", "f8g6", "h8g8", "g6i7", "g8g7", "i7h9", "g7h7"], True, sf.VALUE_DRAW) # Perpetual check vs. intermittent checks - result = sf.is_optional_game_end("xiangqi", "9/3kc4/3a5/3P5/9/4p4/9/4K4/9/3C5 w - - 0 1", 2 * ['d7e7', 'e5d5', 'e7d7', 'd5e5']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "9/3kc4/3a5/3P5/9/4p4/9/4K4/9/3C5 w - - 0 1", 2 * ['d7e7', 'e5d5', 'e7d7', 'd5e5'], True, sf.VALUE_MATE) # Perpetual check by soldier - result = sf.is_optional_game_end("xiangqi", "3k5/9/9/9/9/5p3/9/5p3/5K3/5C3 w - - 0 1", 2 * ['f2e2', 'f3e3', 'e2f2', 'e3f3']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) - result = sf.is_optional_game_end("xiangqi", "3k5/4P4/4b4/3C5/4c4/9/9/9/9/5K3 w - - 0 1", 2 * ['d7e7', 'e8g6', 'e7d7', 'g6e8']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) - result = sf.is_optional_game_end("xiangqi", "3k5/9/9/9/9/9/9/9/cr1CAK3/9 w - - 0 1", 2 * ['d2d4', 'b2b4', 'd4d2', 'b4b2']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) - result = sf.is_optional_game_end("xiangqi", "5k3/9/9/5C3/5c3/5C3/9/9/5p3/4K4 w - - 0 1", 2 * ['f5d5', 'f6d6', 'd5f5', 'd6f6']) - self.assertTrue(result[0]) - self.assertEqual(result[1], -sf.VALUE_MATE) - result = sf.is_optional_game_end("xiangqi", "4k4/9/4b4/2c2nR2/9/9/9/9/9/3K5 w - - 0 1", 2 * ['g7g6', 'f7g9', 'g6g7', 'g9f7']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) - result = sf.is_optional_game_end("xiangqi", "3P5/3k5/3nn4/9/9/9/9/9/9/5K3 w - - 0 1", 2 * ['d10e10', 'd9e9', 'e10d10', 'e9d9']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) - result = sf.is_optional_game_end("xiangqi", "4ck3/9/9/9/9/2r1R4/9/9/4A4/3AK4 w - - 0 1", 2 * ['e5e4', 'c5c4', 'e4e5', 'c4c5']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) - result = sf.is_optional_game_end("xiangqi", "4k4/9/9/c1c6/9/r8/9/9/C8/3K5 w - - 0 1", 2 * ['a2c2', 'a5c5', 'c2a2', 'c5a5']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "3k5/9/9/9/9/5p3/9/5p3/5K3/5C3 w - - 0 1", 2 * ['f2e2', 'f3e3', 'e2f2', 'e3f3'], True, sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "3k5/4P4/4b4/3C5/4c4/9/9/9/9/5K3 w - - 0 1", 2 * ['d7e7', 'e8g6', 'e7d7', 'g6e8'], True, sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "3k5/9/9/9/9/9/9/9/cr1CAK3/9 w - - 0 1", 2 * ['d2d4', 'b2b4', 'd4d2', 'b4b2'], True, sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "5k3/9/9/5C3/5c3/5C3/9/9/5p3/4K4 w - - 0 1", 2 * ['f5d5', 'f6d6', 'd5f5', 'd6f6'], True, -sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "4k4/9/4b4/2c2nR2/9/9/9/9/9/3K5 w - - 0 1", 2 * ['g7g6', 'f7g9', 'g6g7', 'g9f7'], True, sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "3P5/3k5/3nn4/9/9/9/9/9/9/5K3 w - - 0 1", 2 * ['d10e10', 'd9e9', 'e10d10', 'e9d9'], True, sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "4ck3/9/9/9/9/2r1R4/9/9/4A4/3AK4 w - - 0 1", 2 * ['e5e4', 'c5c4', 'e4e5', 'c4c5'], True, sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "4k4/9/9/c1c6/9/r8/9/9/C8/3K5 w - - 0 1", 2 * ['a2c2', 'a5c5', 'c2a2', 'c5a5'], True, sf.VALUE_MATE) # Mutual perpetual check - result = sf.is_optional_game_end("xiangqi", "9/4c4/3k5/3r5/9/9/4C4/9/4K4/3R5 w - - 0 1", 2 * ['e4d4', 'd7e7', 'd4e4', 'e7d7']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_DRAW) - result = sf.is_optional_game_end("xiangqi", "3k5/6c2/9/7P1/6c2/6P2/9/9/9/5K3 w - - 0 1", 2 * ['h7g7', 'g6h6', 'g7h7', 'h6g6']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_DRAW) - result = sf.is_optional_game_end("xiangqi", "4ck3/9/9/9/9/2r1R1N2/6N2/9/4A4/3AK4 w - - 0 1", 2 * ['e5e4', 'c5c4', 'e4e5', 'c4c5']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_DRAW) - result = sf.is_optional_game_end("xiangqi", "5k3/9/9/c8/9/P1P6/9/2C6/9/3K5 w - - 0 1", 2 * ['c3a3', 'a7c7', 'a3c3', 'c7a7']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_DRAW) - result = sf.is_optional_game_end("xiangqi", "4k4/9/r1r6/9/PPPP5/9/9/9/1C7/5K3 w - - 0 1", ['b2a2'] + 2 * ['a8b8', 'a2c2', 'c8d8', 'c2b2', 'b8a8', 'b2d2', 'd8c8', 'd2a2']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_DRAW) + self._check_optional_game_end("xiangqi", "9/4c4/3k5/3r5/9/9/4C4/9/4K4/3R5 w - - 0 1", 2 * ['e4d4', 'd7e7', 'd4e4', 'e7d7'], True, sf.VALUE_DRAW) + self._check_optional_game_end("xiangqi", "3k5/6c2/9/7P1/6c2/6P2/9/9/9/5K3 w - - 0 1", 2 * ['h7g7', 'g6h6', 'g7h7', 'h6g6'], True, sf.VALUE_DRAW) + self._check_optional_game_end("xiangqi", "4ck3/9/9/9/9/2r1R1N2/6N2/9/4A4/3AK4 w - - 0 1", 2 * ['e5e4', 'c5c4', 'e4e5', 'c4c5'], True, sf.VALUE_DRAW) + self._check_optional_game_end("xiangqi", "5k3/9/9/c8/9/P1P6/9/2C6/9/3K5 w - - 0 1", 2 * ['c3a3', 'a7c7', 'a3c3', 'c7a7'], True, sf.VALUE_DRAW) + self._check_optional_game_end("xiangqi", "4k4/9/r1r6/9/PPPP5/9/9/9/1C7/5K3 w - - 0 1", ['b2a2'] + 2 * ['a8b8', 'a2c2', 'c8d8', 'c2b2', 'b8a8', 'b2d2', 'd8c8', 'd2a2'], True, sf.VALUE_DRAW) # Corner cases # D106: Chariot chases cannon, but attack actually does not change (draw) - result = sf.is_optional_game_end("xiangqi", "3k2b2/4P4/4b4/9/8p/6Bc1/6P1P/3AB4/4pp3/1p1K3R1[] w - - 0 1", 2 * ["h1h2", "h5h4", "h2h1", "h4h5"]) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_DRAW) + self._check_optional_game_end("xiangqi", "3k2b2/4P4/4b4/9/8p/6Bc1/6P1P/3AB4/4pp3/1p1K3R1[] w - - 0 1", 2 * ["h1h2", "h5h4", "h2h1", "h4h5"], True, sf.VALUE_DRAW) # D39: Chased chariot pinned by horse + mutual chase (controversial if pinned chariot chases) - result = sf.is_optional_game_end("xiangqi", "2baka1r1/C4rN2/9/1Rp1p4/9/9/4P4/9/4A4/4KA3 w - - 0 1", ["b7b9"] + 2 * ["f10e9", "b9b10", "e9f10", "b10b9"]) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "2baka1r1/C4rN2/9/1Rp1p4/9/9/4P4/9/4A4/4KA3 w - - 0 1", ["b7b9"] + 2 * ["f10e9", "b9b10", "e9f10", "b10b9"], True, sf.VALUE_MATE) # D39: Chased chariot pinned by horse + mutual chase (controversial if pinned chariot chases) - result = sf.is_optional_game_end("xiangqi", "5k3/9/9/9/9/9/7r1/9/2nRA3c/4K4 w - - 0 1", 2 * ['e2f1', 'h4h2', 'f1e2', 'h2h4']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "5k3/9/9/9/9/9/7r1/9/2nRA3c/4K4 w - - 0 1", 2 * ['e2f1', 'h4h2', 'f1e2', 'h2h4'], True, sf.VALUE_MATE) # Creating pins to undermine root - result = sf.is_optional_game_end("xiangqi", "4k4/4c4/9/4p4/9/9/3rn4/3NR4/4K4/9 b - - 0 1", 2 * ['e4g5', 'e2f2', 'g5e4', 'f2e2']) - self.assertTrue(result[0]) - self.assertEqual(result[1], -sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "4k4/4c4/9/4p4/9/9/3rn4/3NR4/4K4/9 b - - 0 1", 2 * ['e4g5', 'e2f2', 'g5e4', 'f2e2'], True, -sf.VALUE_MATE) # Discovered check capture threat by rook - result = sf.is_optional_game_end("xiangqi", "5k3/9/9/9/9/1N2P1C2/9/4BC3/9/cr1RK4 w - - 0 1", 2 * ['b5c3', 'b1c1', 'c3b5', 'c1b1']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "5k3/9/9/9/9/1N2P1C2/9/4BC3/9/cr1RK4 w - - 0 1", 2 * ['b5c3', 'b1c1', 'c3b5', 'c1b1'], True, sf.VALUE_MATE) # Creating a pin to undermine root + discovered check threat by horse - result = sf.is_optional_game_end("xiangqi", "5k3/9/9/9/9/4c4/3n5/3NBA3/4A4/4K4 w - - 0 1", 2 * ['e1d1', 'e5d5', 'd1e1', 'd5e5']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "5k3/9/9/9/9/4c4/3n5/3NBA3/4A4/4K4 w - - 0 1", 2 * ['e1d1', 'e5d5', 'd1e1', 'd5e5'], True, sf.VALUE_MATE) # Creating a pin to undermine root + discovered check threat by rook - result = sf.is_optional_game_end("xiangqi", "5k3/9/9/9/9/4c4/3r5/3NB4/4A4/4K4 w - - 0 1", 2 * ['e1d1', 'e5d5', 'd1e1', 'd5e5']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "5k3/9/9/9/9/4c4/3r5/3NB4/4A4/4K4 w - - 0 1", 2 * ['e1d1', 'e5d5', 'd1e1', 'd5e5'], True, sf.VALUE_MATE) # X-Ray protected discovered check - result = sf.is_optional_game_end("xiangqi", "5k3/9/9/9/9/9/9/9/9/3NK1cr1 w - - 0 1", 2 * ['d1c3', 'h1h3', 'c3d1', 'h3h1']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "5k3/9/9/9/9/9/9/9/9/3NK1cr1 w - - 0 1", 2 * ['d1c3', 'h1h3', 'c3d1', 'h3h1'], True, sf.VALUE_MATE) # No overprotection by king - result = sf.is_optional_game_end("xiangqi", "3k5/9/9/3n5/9/9/3r5/9/9/3NK4 w - - 0 1", 2 * ['d1c3', 'd4c4', 'c3d1', 'c4d4']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_DRAW) + self._check_optional_game_end("xiangqi", "3k5/9/9/3n5/9/9/3r5/9/9/3NK4 w - - 0 1", 2 * ['d1c3', 'd4c4', 'c3d1', 'c4d4'], True, sf.VALUE_DRAW) # Overprotection by king - result = sf.is_optional_game_end("xiangqi", "3k5/9/9/9/9/9/3r5/9/9/3NK4 w - - 0 1", 2 * ['d1c3', 'd4c4', 'c3d1', 'c4d4']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "3k5/9/9/9/9/9/3r5/9/9/3NK4 w - - 0 1", 2 * ['d1c3', 'd4c4', 'c3d1', 'c4d4'], True, sf.VALUE_MATE) # Mutual pins by flying generals - result = sf.is_optional_game_end("xiangqi", "4k4/9/9/9/4n4/9/5C3/9/4N4/4K4 w - - 0 1", 2 * ['e2g1', 'e10f10', 'g1e2', 'f10e10']) - self.assertTrue(result[0]) - #self.assertEqual(result[1], sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "4k4/9/9/9/4n4/9/5C3/9/4N4/4K4 w - - 0 1", 2 * ['e2g1', 'e10f10', 'g1e2', 'f10e10'], True) #, sf.VALUE_MATE) # Fake protection by cannon - result = sf.is_optional_game_end("xiangqi", "5k3/9/9/9/9/1C7/1r7/9/1C7/4K4 w - - 0 1", 2 * ['b5c5', 'b4c4', 'c5b5', 'c4b4']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_MATE) + self._check_optional_game_end("xiangqi", "5k3/9/9/9/9/1C7/1r7/9/1C7/4K4 w - - 0 1", 2 * ['b5c5', 'b4c4', 'c5b5', 'c4b4'], True, sf.VALUE_MATE) # Fake protection by cannon + mutual chase - result = sf.is_optional_game_end("xiangqi", "4ka3/c2R1R2c/4b4/9/9/9/9/9/9/4K4 w - - 0 1", 2 * ['f9f7', 'f10e9', 'f7f9', 'e9f10']) - self.assertTrue(result[0]) - self.assertEqual(result[1], sf.VALUE_DRAW) + self._check_optional_game_end("xiangqi", "4ka3/c2R1R2c/4b4/9/9/9/9/9/9/4K4 w - - 0 1", 2 * ['f9f7', 'f10e9', 'f7f9', 'e9f10'], True, sf.VALUE_DRAW) def test_has_insufficient_material(self): for variant, positions in variant_positions.items(): for fen, expected_result in positions.items(): - result = sf.has_insufficient_material(variant, fen, []) - self.assertEqual(result, expected_result, "{}: {}".format(variant, fen)) + with self.subTest(variant=variant, fen=fen): + result = sf.has_insufficient_material(variant, fen, []) + self.assertEqual(result, expected_result) def test_validate_fen(self): # valid for variant, positions in variant_positions.items(): for fen in positions: - self.assertEqual(sf.validate_fen(fen, variant), sf.FEN_OK, "{}: {}".format(variant, fen)) + with self.subTest(variant=variant, fen=fen): + self.assertEqual(sf.validate_fen(fen, variant), sf.FEN_OK) # 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)) + with self.subTest(variant=variant, fen=fen): + self.assertNotEqual(sf.validate_fen(fen, variant), sf.FEN_OK) # chess960 - self.assertEqual(sf.validate_fen(CHESS960, "chess", True), sf.FEN_OK, "{}: {}".format(variant, fen)) + self.assertEqual(sf.validate_fen(CHESS960, "chess", True), sf.FEN_OK) 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)) + with self.subTest(variant=variant): + fen = sf.start_fen(variant) + self.assertEqual(sf.validate_fen(fen, variant), sf.FEN_OK) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/tests/js/package.json b/tests/js/package.json index cc960f14a..5fbb6c487 100644 --- a/tests/js/package.json +++ b/tests/js/package.json @@ -1,6 +1,6 @@ { "name": "ffish", - "version": "0.7.4", + "version": "0.7.6", "description": "A high performance WebAssembly chess variant library based on Fairy-Stockfish", "main": "ffish.js", "types": "ffish.d.ts", diff --git a/tests/js/test.js b/tests/js/test.js index 88f4c59e7..3c82032c6 100644 --- a/tests/js/test.js +++ b/tests/js/test.js @@ -768,7 +768,7 @@ describe('ffish.setOption(name, value)', function () { }); describe('ffish.setOptionInt(name, value)', function () { - it("it sets a int uci option value pair", () => { + it("it sets an int uci option value pair", () => { ffish.setOptionInt("Threads", 4); chai.expect(true).to.equal(true); });