From 444a92d26934dc3ab9782578250ba0c4ae5f8298 Mon Sep 17 00:00:00 2001 From: Fabian Fichter Date: Sun, 17 Apr 2022 00:15:07 +0200 Subject: [PATCH 1/4] Ignore carriage returns in INI parsing --- src/variant.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/variant.cpp b/src/variant.cpp index 861a1f82..5259a416 100644 --- a/src/variant.cpp +++ b/src/variant.cpp @@ -1482,6 +1482,8 @@ void VariantMap::parse_istream(std::istream& file) { Config attribs = {}; while (file.peek() != '[' && std::getline(file, input)) { + if (!input.empty() && input.back() == '\r') + input.pop_back(); std::stringstream ss(input); if (ss.peek() != ';' && ss.peek() != '#') { From f38948accd6eb340febcfc0db3b7e619146a7f9b Mon Sep 17 00:00:00 2001 From: Fabian Fichter Date: Sun, 17 Apr 2022 12:13:39 +0200 Subject: [PATCH 2/4] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ab2a57b0..1339019f 100644 --- a/README.md +++ b/README.md @@ -85,13 +85,13 @@ The python binding [pyffish](https://pypi.org/project/pyffish/) contributed by [ ### Javascript -The javascript binding ffish.js contributed by [@QueensGambit](https://github.com/QueensGambit) is implemented in [ffishjs.cpp](https://github.com/ianfab/Fairy-Stockfish/blob/master/src/ffishjs.cpp). The compilation/binding to javascript is done using emscripten, see the [readme](https://github.com/ianfab/Fairy-Stockfish/tree/master/tests/js). +The javascript binding [ffish.js](https://www.npmjs.com/package/ffish) contributed by [@QueensGambit](https://github.com/QueensGambit) is implemented in [ffishjs.cpp](https://github.com/ianfab/Fairy-Stockfish/blob/master/src/ffishjs.cpp). The compilation/binding to javascript is done using emscripten, see the [readme](https://github.com/ianfab/Fairy-Stockfish/tree/master/tests/js). ## Ports ### WASM -A port of Fairy-Stockfish to WebAssembly is maintained at https://github.com/ianfab/fairy-stockfish.wasm. It is e.g. used for local analysis on [pychess.org](https://www.pychess.org/). +For in-browser use a [port of Fairy-Stockfish to WebAssembly](https://github.com/ianfab/fairy-stockfish.wasm) is available at [npm](https://www.npmjs.com/package/fairy-stockfish-nnue.wasm). It is e.g. used for local analysis on [pychess.org](https://www.pychess.org/analysis/chess). Also see the [Fairy-Stockfish WASM demo](https://github.com/ianfab/fairy-stockfish-nnue-wasm-demo) available at https://fairy-stockfish-nnue-wasm.vercel.app/. # Stockfish ## Overview From 83e81140894c6cef58e13bd70d5db8b5197b80a4 Mon Sep 17 00:00:00 2001 From: Fabian Fichter Date: Tue, 26 Apr 2022 16:23:00 +0200 Subject: [PATCH 3/4] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1339019f..13773a4c 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ The javascript binding [ffish.js](https://www.npmjs.com/package/ffish) contribut ## Ports -### WASM +### WebAssembly For in-browser use a [port of Fairy-Stockfish to WebAssembly](https://github.com/ianfab/fairy-stockfish.wasm) is available at [npm](https://www.npmjs.com/package/fairy-stockfish-nnue.wasm). It is e.g. used for local analysis on [pychess.org](https://www.pychess.org/analysis/chess). Also see the [Fairy-Stockfish WASM demo](https://github.com/ianfab/fairy-stockfish-nnue-wasm-demo) available at https://fairy-stockfish-nnue-wasm.vercel.app/. From 9022a70549bf741db2fe4b57af42739b1cb91a2d Mon Sep 17 00:00:00 2001 From: Fabian Fichter Date: Wed, 27 Apr 2022 23:27:36 +0200 Subject: [PATCH 4/4] Support Xiangqi chasing rules Add basic support for AXF chasing rules. Some of the more complex cases are not handled yet. Closes #55. --- src/bitboard.h | 5 ++ src/parser.cpp | 9 +++ src/position.cpp | 151 +++++++++++++++++++++++++++++++++++++++++++++-- src/position.h | 2 + src/types.h | 4 ++ src/variant.cpp | 14 +++-- src/variant.h | 1 + src/variants.ini | 2 + test.py | 126 +++++++++++++++++++++++++++++++++++++++ tests/js/test.js | 4 +- 10 files changed, 307 insertions(+), 11 deletions(-) diff --git a/src/bitboard.h b/src/bitboard.h index 6be323ed..60a57a43 100644 --- a/src/bitboard.h +++ b/src/bitboard.h @@ -195,6 +195,11 @@ constexpr bool more_than_one(Bitboard b) { return b & (b - 1); } + +inline Bitboard undo_move_board(Bitboard b, Move m) { + return (from_sq(m) != SQ_NONE && (b & to_sq(m))) ? (b ^ to_sq(m)) | from_sq(m) : b; +} + /// board_size_bb() returns a bitboard representing all the squares /// on a board with given size. diff --git a/src/parser.cpp b/src/parser.cpp index 5ac35968..151e2946 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -95,6 +95,12 @@ namespace { return value == "makruk" || value == "asean" || value == "none"; } + template <> bool set(const std::string& value, ChasingRule& target) { + target = value == "axf" ? AXF_CHASING + : NO_CHASING; + return value == "axf" || value == "none"; + } + template <> bool set(const std::string& value, EnclosingRule& target) { target = value == "reversi" ? REVERSI : value == "ataxx" ? ATAXX @@ -129,6 +135,8 @@ template void VariantParser::parse_attribute(const std::strin : std::is_same() ? "Value" : std::is_same() ? "MaterialCounting" : std::is_same() ? "CountingRule" + : std::is_same() ? "ChasingRule" + : std::is_same() ? "EnclosingRule" : std::is_same() ? "Bitboard" : typeid(T).name(); std::cerr << key << " - Invalid value " << it->second << " for type " << typeName << std::endl; @@ -335,6 +343,7 @@ Variant* VariantParser::parse(Variant* v) { parse_attribute("nFoldValueAbsolute", v->nFoldValueAbsolute); parse_attribute("perpetualCheckIllegal", v->perpetualCheckIllegal); parse_attribute("moveRepetitionIllegal", v->moveRepetitionIllegal); + parse_attribute("chasingRule", v->chasingRule); parse_attribute("stalemateValue", v->stalemateValue); parse_attribute("stalematePieceCount", v->stalematePieceCount); parse_attribute("checkmateValue", v->checkmateValue); diff --git a/src/position.cpp b/src/position.cpp index 96b4dfb4..7009ff6d 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -96,6 +96,10 @@ std::ostream& operator<<(std::ostream& os, const Position& pos) { for (Bitboard b = pos.checkers(); b; ) os << UCI::square(pos, pop_lsb(b)) << " "; + os << "\nChased: "; + for (Bitboard b = pos.state()->chased; b; ) + os << UCI::square(pos, pop_lsb(b)) << " "; + if ( int(Tablebases::MaxCardinality) >= popcount(pos.pieces()) && Options["UCI_Variant"] == "chess" && !pos.can_castle(ANY_CASTLING)) @@ -538,6 +542,7 @@ void Position::set_check_info(StateInfo* si) const { } si->shak = si->checkersBB & (byTypeBB[KNIGHT] | byTypeBB[ROOK] | byTypeBB[BERS]); si->bikjang = var->bikjangRule && ksq != SQ_NONE ? bool(attacks_bb(sideToMove, ROOK, ksq, pieces()) & pieces(sideToMove, KING)) : false; + si->chased = var->chasingRule ? chased() : Bitboard(0); si->legalCapture = NO_VALUE; if (var->extinctionPseudoRoyal) { @@ -564,6 +569,7 @@ void Position::set_state(StateInfo* si) const { si->pawnKey = Zobrist::noPawns; si->nonPawnMaterial[WHITE] = si->nonPawnMaterial[BLACK] = VALUE_ZERO; si->checkersBB = count(sideToMove) ? attackers_to(square(sideToMove), ~sideToMove) : Bitboard(0); + si->move = MOVE_NONE; set_check_info(si); @@ -784,10 +790,14 @@ Bitboard Position::slider_blockers(Bitboard sliders, Square s, Bitboard& pinners // Snipers are sliders that attack 's' when a piece and other snipers are removed Bitboard snipers = 0; + Bitboard slidingSnipers = 0; if (var->fastAttacks) + { snipers = ( (attacks_bb< ROOK>(s) & pieces(c, QUEEN, ROOK, CHANCELLOR)) | (attacks_bb(s) & pieces(c, QUEEN, BISHOP, ARCHBISHOP))) & sliders; + slidingSnipers = snipers; + } else for (PieceType pt : piece_types()) { @@ -807,16 +817,19 @@ Bitboard Position::slider_blockers(Bitboard sliders, Square s, Bitboard& pinners } else snipers |= b & ~attacks_bb(~c, pt, s, pieces()); + if (AttackRiderTypes[pt] & ~HOPPING_RIDERS) + slidingSnipers |= snipers & pieces(pt); } } - Bitboard occupancy = pieces() ^ snipers; + Bitboard occupancy = pieces() ^ slidingSnipers; while (snipers) { Square sniperSq = pop_lsb(snipers); - Bitboard b = between_bb(s, sniperSq, type_of(piece_on(sniperSq))) & occupancy; + bool isHopper = AttackRiderTypes[type_of(piece_on(sniperSq))] & HOPPING_RIDERS; + Bitboard b = between_bb(s, sniperSq, type_of(piece_on(sniperSq))) & (isHopper ? (pieces() ^ sniperSq) : occupancy); - if (b && (!more_than_one(b) || ((AttackRiderTypes[type_of(piece_on(sniperSq))] & HOPPING_RIDERS) && popcount(b) == 2))) + if (b && (!more_than_one(b) || (isHopper && popcount(b) == 2))) { // Janggi cannons block each other if ((pieces(JANGGI_CANNON) & sniperSq) && (pieces(JANGGI_CANNON) & b)) @@ -2316,6 +2329,8 @@ bool Position::is_optional_game_end(Value& result, int ply, int countStarted) co int cnt = 0; bool perpetualThem = st->checkersBB && stp->checkersBB; bool perpetualUs = st->previous->checkersBB && stp->previous->checkersBB; + Bitboard chaseThem = undo_move_board(st->chased, st->previous->move) & stp->chased; + Bitboard chaseUs = undo_move_board(st->previous->chased, stp->move) & stp->previous->chased; int moveRepetition = var->moveRepetitionIllegal && type_of(st->move) == NORMAL && !st->previous->checkersBB && !stp->previous->checkersBB @@ -2348,6 +2363,9 @@ bool Position::is_optional_game_end(Value& result, int ply, int countStarted) co moveRepetition = 0; } } + // Chased pieces are empty when there is no previous move + if (i != st->pliesFromNull) + chaseThem = undo_move_board(chaseThem, stp->previous->move) & stp->previous->previous->chased; stp = stp->previous->previous; perpetualThem &= bool(stp->checkersBB); @@ -2356,8 +2374,8 @@ bool Position::is_optional_game_end(Value& result, int ply, int countStarted) co if ( stp->key == st->key && ++cnt + 1 == (ply > i && !var->moveRepetitionIllegal ? 2 : n_fold_rule())) { - result = convert_mate_value( var->perpetualCheckIllegal && perpetualThem ? VALUE_MATE - : var->perpetualCheckIllegal && perpetualUs ? -VALUE_MATE + result = convert_mate_value( var->perpetualCheckIllegal && (perpetualThem || perpetualUs) ? (!perpetualUs ? VALUE_MATE : !perpetualThem ? -VALUE_MATE : VALUE_DRAW) + : var->chasingRule && (chaseThem || chaseUs) ? (!chaseUs ? VALUE_MATE : !chaseThem ? -VALUE_MATE : VALUE_DRAW) : var->nFoldValueAbsolute && sideToMove == BLACK ? -var->nFoldValue : var->nFoldValue, ply); if (result == VALUE_DRAW && var->materialCounting) @@ -2366,7 +2384,10 @@ bool Position::is_optional_game_end(Value& result, int ply, int countStarted) co } if (i + 1 <= end) + { perpetualUs &= bool(stp->previous->checkersBB); + chaseUs = undo_move_board(chaseUs, stp->move) & stp->previous->chased; + } } } } @@ -2507,6 +2528,126 @@ bool Position::is_immediate_game_end(Value& result, int ply) const { return false; } +// Position::chased() tests whether the last move was a chase. + +Bitboard Position::chased() const { + Bitboard b = 0; + if (st->move == MOVE_NONE) + return b; + + Bitboard pins = blockers_for_king(sideToMove); + if (var->flyingGeneral) + { + Bitboard kingFilePieces = file_bb(file_of(square(~sideToMove))) & pieces(sideToMove); + if ((kingFilePieces & pieces(sideToMove, KING)) && !more_than_one(kingFilePieces & ~pieces(KING))) + pins |= kingFilePieces & ~pieces(KING); + } + auto addChased = [&](Square attackerSq, PieceType attackerType, Bitboard attacks) { + if (attacks & ~b) + { + // Exclude attacks on unpromoted soldiers and checks + attacks &= ~(pieces(sideToMove, KING, SOLDIER) ^ promoted_soldiers(sideToMove)); + // Attacks against stronger pieces + if (attackerType == HORSE || attackerType == CANNON) + b |= attacks & pieces(sideToMove, ROOK); + if (attackerType == ELEPHANT || attackerType == FERS) + b |= attacks & pieces(sideToMove, ROOK, CANNON, HORSE); + // Exclude mutual/symmetric attacks + // Exceptions: + // - asymmetric pieces ("impaired horse") + // - pins + if (attackerType == HORSE && (PseudoAttacks[WHITE][FERS][attackerSq] & pieces())) + { + Bitboard horses = attacks & pieces(sideToMove, attackerType); + while (horses) + { + Square s = pop_lsb(horses); + if (attacks_bb(sideToMove, attackerType, s, pieces()) & attackerSq) + attacks ^= s; + } + } + else + attacks &= ~pieces(sideToMove, attackerType) | pins; + // Attacks against potentially unprotected pieces + while (attacks) + { + Square s = pop_lsb(attacks); + Bitboard roots = attackers_to(s, pieces() ^ attackerSq, sideToMove) & ~pins; + if (!roots || (var->flyingGeneral && roots == pieces(sideToMove, KING) && (attacks_bb(sideToMove, ROOK, square(~sideToMove), pieces() ^ attackerSq) & s))) + b |= s; + } + } + }; + + // Direct attacks + Square from = from_sq(st->move); + Square to = to_sq(st->move); + PieceType movedPiece = type_of(piece_on(to)); + if (movedPiece != KING && movedPiece != SOLDIER) + { + Bitboard directAttacks = attacks_from(~sideToMove, movedPiece, to) & pieces(sideToMove); + // Only new attacks count. This avoids expensive comparison of previous and new attacks. + if (movedPiece == ROOK || movedPiece == CANNON) + directAttacks &= ~line_bb(from, to); + addChased(to, movedPiece, directAttacks); + } + + // Discovered attacks + Bitboard discoveryCandidates = (PseudoAttacks[WHITE][WAZIR][from] & pieces(~sideToMove, HORSE)) + | (PseudoAttacks[WHITE][FERS][from] & pieces(~sideToMove, ELEPHANT)) + | (PseudoAttacks[WHITE][ROOK][from] & pieces(~sideToMove, CANNON, ROOK)) + | (PseudoAttacks[WHITE][ROOK][to] & pieces(~sideToMove, CANNON)); + while (discoveryCandidates) + { + Square s = pop_lsb(discoveryCandidates); + PieceType discoveryPiece = type_of(piece_on(s)); + Bitboard discoveries = pieces(sideToMove) + & attacks_bb(~sideToMove, discoveryPiece, s, pieces()) + & ~attacks_bb(~sideToMove, discoveryPiece, s, (captured_piece() ? pieces() : pieces() ^ to) ^ from); + addChased(s, discoveryPiece, discoveries); + } + + // Changes in real roots and discovered checks + if (st->pliesFromNull > 0) + { + // Fake roots + Bitboard newPins = st->blockersForKing[sideToMove] & ~st->previous->blockersForKing[sideToMove] & pieces(sideToMove); + while (newPins) + { + Square s = pop_lsb(newPins); + PieceType pinnedPiece = type_of(piece_on(s)); + Bitboard fakeRooted = pieces(sideToMove) + & ~(pieces(sideToMove, KING, SOLDIER) ^ promoted_soldiers(sideToMove)) + & attacks_bb(sideToMove, pinnedPiece, s, pieces()); + while (fakeRooted) + { + Square s2 = pop_lsb(fakeRooted); + if (attackers_to(s2, ~sideToMove) & ~blockers_for_king(~sideToMove)) + b |= s2; + } + } + // Discovered checks + Bitboard newDiscoverers = st->blockersForKing[sideToMove] & ~st->previous->blockersForKing[sideToMove] & pieces(~sideToMove); + while (newDiscoverers) + { + Square s = pop_lsb(newDiscoverers); + PieceType discoveryPiece = type_of(piece_on(s)); + Bitboard discoveryAttacks = attacks_from(~sideToMove, discoveryPiece, s) & pieces(sideToMove); + // Include all captures except where the king can pseudo-legally recapture + b |= discoveryAttacks & ~attacks_from(sideToMove, KING, square(sideToMove)); + // Include captures where king can not legally recapture + discoveryAttacks &= attacks_from(sideToMove, KING, square(sideToMove)); + while (discoveryAttacks) + { + Square s2 = pop_lsb(discoveryAttacks); + if (attackers_to(s2, pieces() ^ s ^ square(sideToMove), ~sideToMove) & ~square_bb(s)) + b |= s2; + } + } + } + + return b; +} // Position::has_repeated() tests whether there has been at least one repetition // of positions since the last capture or pawn move. diff --git a/src/position.h b/src/position.h index 3d232bd9..569281db 100644 --- a/src/position.h +++ b/src/position.h @@ -75,6 +75,7 @@ struct StateInfo { bool capturedpromoted; bool shak; bool bikjang; + Bitboard chased; bool pass; Move move; int repetition; @@ -298,6 +299,7 @@ class Position { bool is_draw(int ply) const; bool has_game_cycle(int ply) const; bool has_repeated() const; + Bitboard chased() const; int counting_limit() const; int counting_ply(int countStarted) const; int rule50_count() const; diff --git a/src/types.h b/src/types.h index 43d79ad0..084b4920 100644 --- a/src/types.h +++ b/src/types.h @@ -296,6 +296,10 @@ enum CountingRule { NO_COUNTING, MAKRUK_COUNTING, ASEAN_COUNTING }; +enum ChasingRule { + NO_CHASING, AXF_CHASING +}; + enum EnclosingRule { NO_ENCLOSING, REVERSI, ATAXX }; diff --git a/src/variant.cpp b/src/variant.cpp index 5259a416..2baac0fd 100644 --- a/src/variant.cpp +++ b/src/variant.cpp @@ -1261,7 +1261,8 @@ namespace { #endif // Xiangqi (Chinese chess) // https://en.wikipedia.org/wiki/Xiangqi - Variant* xiangqi_variant() { + // Xiangqi base variant for inheriting rules without chasing rules + Variant* xiangqi_variant_base() { Variant* v = minixiangqi_variant()->init(); v->pieceToCharTable = "PN.R.AB..K.C..........pn.r.ab..k.c.........."; v->maxRank = RANK_10; @@ -1278,11 +1279,16 @@ namespace { v->soldierPromotionRank = RANK_6; return v; } + Variant* xiangqi_variant() { + Variant* v = xiangqi_variant_base()->init(); + v->chasingRule = AXF_CHASING; + return v; + } // Manchu/Yitong chess // Asymmetric Xiangqi variant with a super-piece // https://en.wikipedia.org/wiki/Manchu_chess Variant* manchu_variant() { - Variant* v = xiangqi_variant()->init(); + Variant* v = xiangqi_variant_base()->init(); v->pieceToCharTable = "PN.R.AB..K.C....M.....pn.r.ab..k.c.........."; v->add_piece(BANNER, 'm'); v->startFen = "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/9/9/M1BAKAB2 w - - 0 1"; @@ -1291,7 +1297,7 @@ namespace { // Supply chess // https://en.wikipedia.org/wiki/Xiangqi#Variations Variant* supply_variant() { - Variant* v = xiangqi_variant()->init(); + Variant* v = xiangqi_variant_base()->init(); v->variantTemplate = "bughouse"; v->startFen = "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR[] w - - 0 1"; v->twoBoards = true; @@ -1305,7 +1311,7 @@ namespace { // https://en.wikipedia.org/wiki/Janggi // Official tournament rules with bikjang and material counting. Variant* janggi_variant() { - Variant* v = xiangqi_variant()->init(); + Variant* v = xiangqi_variant_base()->init(); v->variantTemplate = "janggi"; v->pieceToCharTable = ".N.R.AB.P..C.........K.n.r.ab.p..c.........k"; v->remove_piece(FERS); diff --git a/src/variant.h b/src/variant.h index ffbbcbf8..ebb98765 100644 --- a/src/variant.h +++ b/src/variant.h @@ -113,6 +113,7 @@ struct Variant { bool nFoldValueAbsolute = false; bool perpetualCheckIllegal = false; bool moveRepetitionIllegal = false; + ChasingRule chasingRule = NO_CHASING; Value stalemateValue = VALUE_DRAW; bool stalematePieceCount = false; // multiply stalemate value by sign(count(~stm) - count(stm)) Value checkmateValue = -VALUE_MATE; diff --git a/src/variants.ini b/src/variants.ini index 7683cfa7..ac2348dc 100644 --- a/src/variants.ini +++ b/src/variants.ini @@ -127,6 +127,7 @@ # [Value]: game result for the side to move [win, loss, draw] # [MaterialCounting]: material couting rules for adjudication [janggi, unweighted, whitedrawodds, blackdrawodds, none] # [CountingRule]: makruk or ASEAN counting rules [makruk, asean, none] +# [ChasingRule]: xiangqi chasing rules [axf, none] # [EnclosingRule]: reversi or ataxx enclosing rules [reversi, ataxx, none] ### Additional options relevant for usage in Winboard/XBoard @@ -206,6 +207,7 @@ # nFoldValueAbsolute: result in case of 3/n-fold repetition is from white's point of view [bool] (default: false) # perpetualCheckIllegal: prohibit perpetual checks [bool] (default: false) # moveRepetitionIllegal: prohibit moving back and forth with the same piece nFoldRule-1 times [bool] (default: false) +# chasingRule: enable chasing rules [ChasingRule] (default: none) # stalemateValue: result in case of stalemate [Value] (default: draw) # stalematePieceCount: count material in case of stalemate [bool] (default: false) # checkmateValue: result in case of checkmate [Value] (default: loss) diff --git a/test.py b/test.py index 29968624..6b9cb91f 100644 --- a/test.py +++ b/test.py @@ -779,6 +779,132 @@ def test_is_optional_game_end(self): self.assertTrue(result[0]) self.assertEqual(result[1], 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) + # 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) + # 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) + # 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) + # 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) + # 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) + # 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) + # 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) + # 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) + + # 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) + # 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) + # 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) + # 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) + # 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) + # 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) + # 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) + # 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) + # 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) + # 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) + # 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) + # 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) + # 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) + def test_has_insufficient_material(self): for variant, positions in variant_positions.items(): for fen, expected_result in positions.items(): diff --git a/tests/js/test.js b/tests/js/test.js index d7598b6b..c4dc3660 100644 --- a/tests/js/test.js +++ b/tests/js/test.js @@ -605,7 +605,7 @@ describe('board.toVerboseString()', function () { " a b c d e f g h\n\n" + "Fen: rnb1kbnr/ppp1pppp/8/3q4/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 3\n" + "Sfen: rnb1kbnr/ppp1pppp/8/3q4/8/8/PPPP1PPP/RNBQKBNR b - 5\n" + - "Key: 39B6F80E84D75BFB\nCheckers: ") + "Key: 39B6F80E84D75BFB\nCheckers: \nChased: ") board.delete(); const board2 = new ffish.Board("xiangqi"); chai.expect(board2.toVerboseString()).to.equal("\n +---+---+---+---+---+---+---+---+---+\n" + @@ -632,7 +632,7 @@ describe('board.toVerboseString()', function () { " a b c d e f g h i\n\n" + "Fen: rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1\n" + "Sfen: rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR b - 1\n" + - "Key: CF494C075A7D927E\nCheckers: "); + "Key: CF494C075A7D927E\nCheckers: \nChased: "); board2.delete(); }); });