Add basic support for AXF chasing rules.
Some of the more complex cases are not handled yet.
Closes #55.
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.
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
: std::is_same<T, Value>() ? "Value"
: std::is_same<T, MaterialCounting>() ? "MaterialCounting"
: std::is_same<T, CountingRule>() ? "CountingRule"
+ : std::is_same<T, ChasingRule>() ? "ChasingRule"
+ : std::is_same<T, EnclosingRule>() ? "EnclosingRule"
: std::is_same<T, Bitboard>() ? "Bitboard"
: typeid(T).name();
std::cerr << key << " - Invalid value " << it->second << " for type " << typeName << std::endl;
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);
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))
}
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)
{
si->pawnKey = Zobrist::noPawns;
si->nonPawnMaterial[WHITE] = si->nonPawnMaterial[BLACK] = VALUE_ZERO;
si->checkersBB = count<KING>(sideToMove) ? attackers_to(square<KING>(sideToMove), ~sideToMove) : Bitboard(0);
+ si->move = MOVE_NONE;
set_check_info(si);
// 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<BISHOP>(s) & pieces(c, QUEEN, BISHOP, ARCHBISHOP))) & sliders;
+ slidingSnipers = snipers;
+ }
else
for (PieceType pt : piece_types())
{
}
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))
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
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);
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)
}
if (i + 1 <= end)
+ {
perpetualUs &= bool(stp->previous->checkersBB);
+ chaseUs = undo_move_board(chaseUs, stp->move) & stp->previous->chased;
+ }
}
}
}
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<KING>(~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<KING>(~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<KING>(sideToMove));
+ // Include captures where king can not legally recapture
+ discoveryAttacks &= attacks_from(sideToMove, KING, square<KING>(sideToMove));
+ while (discoveryAttacks)
+ {
+ Square s2 = pop_lsb(discoveryAttacks);
+ if (attackers_to(s2, pieces() ^ s ^ square<KING>(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.
bool capturedpromoted;
bool shak;
bool bikjang;
+ Bitboard chased;
bool pass;
Move move;
int repetition;
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;
NO_COUNTING, MAKRUK_COUNTING, ASEAN_COUNTING
};
+enum ChasingRule {
+ NO_CHASING, AXF_CHASING
+};
+
enum EnclosingRule {
NO_ENCLOSING, REVERSI, ATAXX
};
#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;
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";
// 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;
// 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);
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;
# [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
# 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)
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():
" 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" +
" 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();
});
});