From 19d469a4c86cd2c9551fa8e6666ff9f8621c93d4 Mon Sep 17 00:00:00 2001 From: Fabian Fichter Date: Sun, 5 Apr 2020 16:12:03 +0200 Subject: [PATCH] Support more notation systems in pyffish Add long algebraic notation and western notations for Shogi, Xiangqi, and Janggi. - Shogi Hodges and Hosking - Xiangqi WXF - Janggi --- src/pyffish.cpp | 387 ++++++++++++++++++++++++++++++++++++++----------------- test.py | 47 ++++++- 2 files changed, 309 insertions(+), 125 deletions(-) diff --git a/src/pyffish.cpp b/src/pyffish.cpp index 4625c69..9a45089 100644 --- a/src/pyffish.cpp +++ b/src/pyffish.cpp @@ -24,106 +24,244 @@ namespace PSQT { void init(const Variant* v); } -using namespace std; - namespace { -const string move_to_san(Position& pos, Move m, const bool shogi) { - Bitboard others, b; - string san; - Color us = pos.side_to_move(); - Square from = from_sq(m); - Square to = to_sq(m); - Piece pc = pos.piece_on(from); - PieceType pt = type_of(pc); +enum Notation { + NOTATION_DEFAULT, + // https://en.wikipedia.org/wiki/Algebraic_notation_(chess) + NOTATION_SAN, + NOTATION_LAN, + // https://en.wikipedia.org/wiki/Shogi_notation#Western_notation + NOTATION_SHOGI_HOSKING, // Examples: P76, S’34 + NOTATION_SHOGI_HODGES, // Examples: P-7f, S*3d + // http://www.janggi.pl/janggi-notation/ + NOTATION_JANGGI, + // https://en.wikipedia.org/wiki/Xiangqi#Notation + NOTATION_XIANGQI_WXF, +}; + +Notation default_notation(const Variant* v) { + if (v->variantTemplate == "shogi") + return NOTATION_SHOGI_HODGES; + return NOTATION_SAN; +} + +enum Disambiguation { + NO_DISAMBIGUATION, + FILE_DISAMBIGUATION, + RANK_DISAMBIGUATION, + SQUARE_DISAMBIGUATION, +}; + +bool is_shogi(Notation n) { + return n == NOTATION_SHOGI_HOSKING || n == NOTATION_SHOGI_HODGES; +} + +std::string piece(const Position& pos, Move m, Notation n) { + Color us = pos.side_to_move(); + Square from = from_sq(m); + Piece pc = pos.moved_piece(m); + PieceType pt = type_of(pc); + // Quiet pawn moves + if ((n == NOTATION_SAN || n == NOTATION_LAN) && type_of(pc) == PAWN && type_of(m) != DROP) + return ""; + // Tandem pawns + else if (n == NOTATION_XIANGQI_WXF && popcount(pos.pieces(us, pt) & file_bb(from)) > 2) + return std::to_string(popcount(forward_file_bb(us, from) & pos.pieces(us, pt)) + 1); + // Moves of promoted pieces + else if (type_of(m) != DROP && pos.unpromoted_piece_on(from)) + return "+" + std::string(1, toupper(pos.piece_to_char()[pos.unpromoted_piece_on(from)])); + // Promoted drops + else if (type_of(m) == DROP && dropped_piece_type(m) != in_hand_piece_type(m)) + return "+" + std::string(1, toupper(pos.piece_to_char()[in_hand_piece_type(m)])); + else if (pos.piece_to_char_synonyms()[pc] != ' ') + return std::string(1, toupper(pos.piece_to_char_synonyms()[pc])); + else + return std::string(1, toupper(pos.piece_to_char()[pc])); +} + +std::string file(const Position& pos, Square s, Notation n) { + switch (n) { + case NOTATION_SHOGI_HOSKING: + case NOTATION_SHOGI_HODGES: + return std::to_string(pos.max_file() - file_of(s) + 1); + case NOTATION_JANGGI: + return std::to_string(file_of(s) + 1); + case NOTATION_XIANGQI_WXF: + return std::to_string((pos.side_to_move() == WHITE ? pos.max_file() - file_of(s) : file_of(s)) + 1); + default: + return std::string(1, char('a' + file_of(s))); + } +} + +std::string rank(const Position& pos, Square s, Notation n) { + switch (n) { + case NOTATION_SHOGI_HOSKING: + return std::to_string(pos.max_rank() - rank_of(s) + 1); + case NOTATION_SHOGI_HODGES: + return std::string(1, char('a' + pos.max_rank() - rank_of(s))); + case NOTATION_JANGGI: + return std::to_string((pos.max_rank() - rank_of(s) + 1) % 10); + case NOTATION_XIANGQI_WXF: + { + if (pos.empty(s)) + return std::to_string(relative_rank(pos.side_to_move(), s, pos.max_rank()) + 1); + else if (pos.pieces(pos.side_to_move(), type_of(pos.piece_on(s))) & forward_file_bb(pos.side_to_move(), s)) + return "-"; + else + return "+"; + } + default: + return std::to_string(rank_of(s) + 1); + } +} + +std::string square(const Position& pos, Square s, Notation n) { + switch (n) { + case NOTATION_JANGGI: + return rank(pos, s, n) + file(pos, s, n); + default: + return file(pos, s, n) + rank(pos, s, n); + } +} + +Disambiguation disambiguation_level(const Position& pos, Move m, Notation n) { + // Drops never need disambiguation + if (type_of(m) == DROP) + return NO_DISAMBIGUATION; + + // NOTATION_LAN and Janggi always use disambiguation + if (n == NOTATION_LAN || n == NOTATION_JANGGI) + return SQUARE_DISAMBIGUATION; + + Color us = pos.side_to_move(); + Square from = from_sq(m); + Square to = to_sq(m); + Piece pc = pos.moved_piece(m); + PieceType pt = type_of(pc); + + // Xiangqi uses either file disambiguation or +/- if two pieces on file + if (n == NOTATION_XIANGQI_WXF) + return popcount(file_bb(from) & pos.pieces(us, pt)) == 2 ? RANK_DISAMBIGUATION : FILE_DISAMBIGUATION; + + // Pawn captures always use disambiguation + if ((n == NOTATION_SAN || n == NOTATION_LAN) && pt == PAWN && pos.capture(m) && from != to) + return FILE_DISAMBIGUATION; + + // A disambiguation occurs if we have more then one piece of type 'pt' + // that can reach 'to' with a legal move. + Bitboard others, b; + others = b = ((pos.capture(m) ? attacks_bb(~us, pt == HORSE ? KNIGHT : pt, to, pos.pieces()) + : moves_bb( ~us, pt == HORSE ? KNIGHT : pt, to, pos.pieces())) & pos.pieces(us, pt)) & ~square_bb(from); + + while (b) + { + Square s = pop_lsb(&b); + if ( !pos.pseudo_legal(make_move(s, to)) + || !pos.legal(make_move(s, to)) + || (is_shogi(n) && pos.unpromoted_piece_on(s) != pos.unpromoted_piece_on(from))) + others ^= s; + } + + if (!others) + return NO_DISAMBIGUATION; + else if (is_shogi(n)) + return SQUARE_DISAMBIGUATION; + else if (!(others & file_bb(from))) + return FILE_DISAMBIGUATION; + else if (!(others & rank_bb(from))) + return RANK_DISAMBIGUATION; + else + return SQUARE_DISAMBIGUATION; +} + +std::string disambiguation(const Position& pos, Square s, Notation n, Disambiguation d) { + switch (d) + { + case FILE_DISAMBIGUATION: + return file(pos, s, n); + case RANK_DISAMBIGUATION: + return rank(pos, s, n); + case SQUARE_DISAMBIGUATION: + return square(pos, s, n); + default: + assert(d == NO_DISAMBIGUATION); + return ""; + } +} - string protocol = Options["Protocol"]; - Options["Protocol"] = string(shogi ? "usi" : "uci"); +const std::string move_to_san(Position& pos, Move m, Notation n) { + std::string san = ""; + Color us = pos.side_to_move(); + Square from = from_sq(m); + Square to = to_sq(m); - if (type_of(m) == CASTLING) - { - san = to > from ? "O-O" : "O-O-O"; + if (type_of(m) == CASTLING) + { + san = to > from ? "O-O" : "O-O-O"; if (is_gating(m)) { - san += string("/") + pos.piece_to_char()[make_piece(WHITE, gating_type(m))]; - san += gating_square(m) == to ? UCI::square(pos, to) : UCI::square(pos, from); + san += std::string("/") + pos.piece_to_char()[make_piece(WHITE, gating_type(m))]; + san += square(pos, gating_square(m), n); + } + } + else + { + // Piece + san += piece(pos, m, n); + + // Origin square, disambiguation + Disambiguation d = disambiguation_level(pos, m, n); + san += disambiguation(pos, from, n, d); + + // Separator/Operator + if (type_of(m) == DROP) + san += (n == NOTATION_SHOGI_HODGES ? '*' : n == NOTATION_SHOGI_HOSKING ? '\'' : '@'); + else if (n == NOTATION_XIANGQI_WXF) + { + if (rank_of(from) == rank_of(to)) + san += '='; + else if (relative_rank(us, to, pos.max_rank()) > relative_rank(us, from, pos.max_rank())) + san += '+'; + else + san += '-'; } - } - else - { - if (type_of(m) == DROP) - san += UCI::dropped_piece(pos, m) + (shogi ? '*' : '@'); - else - { - if (pt != PAWN) - { - if (pos.is_promoted(from) && shogi) - san += "+" + string(1, toupper(pos.piece_to_char()[pos.unpromoted_piece_on(from)])); - else if (pos.piece_to_char_synonyms()[make_piece(WHITE, pt)] != ' ') - san += pos.piece_to_char_synonyms()[make_piece(WHITE, pt)]; - else - san += pos.piece_to_char()[make_piece(WHITE, pt)]; - - // A disambiguation occurs if we have more then one piece of type 'pt' - // that can reach 'to' with a legal move. - others = b = ((pos.capture(m) ? attacks_bb(~us, pt == HORSE ? KNIGHT : pt, to, pos.pieces()) - : moves_bb(~us, pt == HORSE ? KNIGHT : pt, to, pos.pieces())) & pos.pieces(us, pt)) ^ from; - - while (b) - { - Square s = pop_lsb(&b); - if ( !pos.pseudo_legal(make_move(s, to)) - || !pos.legal(make_move(s, to)) - || (shogi && pos.unpromoted_piece_on(s) != pos.unpromoted_piece_on(from))) - others ^= s; - } - - if (!others) - { /* disambiguation is not needed */ } - else if (!(others & file_bb(from)) && !shogi) - san += UCI::square(pos, from)[0]; - else if (!(others & rank_bb(from)) && !shogi) - san += UCI::square(pos, from)[1]; - else - san += UCI::square(pos, from); - } - - if (pos.capture(m) && from != to) - { - if (pt == PAWN) - san += UCI::square(pos, from)[0]; - san += 'x'; - } - else if (shogi) - san += '-'; - } - - san += UCI::square(pos, to); - - if (type_of(m) == PROMOTION) - san += string("=") + pos.piece_to_char()[make_piece(WHITE, promotion_type(m))]; - else if (type_of(m) == PIECE_PROMOTION) - san += string("+"); - else if (type_of(m) == PIECE_DEMOTION) - san += string("-"); - else if (type_of(m) == NORMAL && shogi && pos.pseudo_legal(make(from, to))) - san += string("="); - else if (is_gating(m)) - san += string("/") + pos.piece_to_char()[make_piece(WHITE, gating_type(m))]; - } - - if (pos.gives_check(m) && (!shogi)) - { - StateInfo st; - pos.do_move(m, st); - san += MoveList(pos).size() ? "+" : "#"; - pos.undo_move(m); - } - // reset protocol - Options["Protocol"] = protocol; - return san; + else if (pos.capture(m) && from != to) + san += 'x'; + else if (n == NOTATION_LAN || n == NOTATION_SHOGI_HODGES || (n == NOTATION_SHOGI_HOSKING && d == SQUARE_DISAMBIGUATION) || n == NOTATION_JANGGI) + san += '-'; + + // Destination square + if (n == NOTATION_XIANGQI_WXF && type_of(m) != DROP) + san += file_of(to) == file_of(from) ? std::to_string(std::abs(rank_of(to) - rank_of(from))) : file(pos, to, n); + else + san += square(pos, to, n); + + // Suffix + if (type_of(m) == PROMOTION) + san += std::string("=") + pos.piece_to_char()[make_piece(WHITE, promotion_type(m))]; + else if (type_of(m) == PIECE_PROMOTION) + san += std::string("+"); + else if (type_of(m) == PIECE_DEMOTION) + san += std::string("-"); + else if (type_of(m) == NORMAL && is_shogi(n) && pos.pseudo_legal(make(from, to))) + san += std::string("="); + if (is_gating(m)) + san += std::string("/") + pos.piece_to_char()[make_piece(WHITE, gating_type(m))]; + } + + // Check and checkmate + if (pos.gives_check(m) && !is_shogi(n)) + { + StateInfo st; + pos.do_move(m, st); + san += MoveList(pos).size() ? "+" : "#"; + pos.undo_move(m); + } + + return san; } bool hasInsufficientMaterial(Color c, const Position& pos) { @@ -169,18 +307,17 @@ bool hasInsufficientMaterial(Color c, const Position& pos) { void buildPosition(Position& pos, StateListPtr& states, const char *variant, const char *fen, PyObject *moveList, const bool chess960) { states = StateListPtr(new std::deque(1)); // Drop old and create a new one - const Variant* v = variants.find(string(variant))->second; + const Variant* v = variants.find(std::string(variant))->second; if (strcmp(fen, "startpos") == 0) fen = v->startFen.c_str(); - bool sfen = false; - Options["Protocol"] = string("uci"); - Options["UCI_Chess960"] = (chess960) ? UCI::Option(true) : UCI::Option(false); - pos.set(v, string(fen), Options["UCI_Chess960"], &states->back(), Threads.main(), sfen); + Options["UCI_Chess960"] = chess960; + pos.set(v, std::string(fen), chess960, &states->back(), Threads.main()); // parse move list int numMoves = PyList_Size(moveList); - for (int i=0; iback()); } else - { - PyErr_SetString(PyExc_ValueError, (string("Invalid move '")+moveStr+"'").c_str()); - } + PyErr_SetString(PyExc_ValueError, (std::string("Invalid move '") + moveStr + "'").c_str()); } return; } @@ -209,10 +344,10 @@ extern "C" PyObject* pyffish_setOption(PyObject* self, PyObject *args) { if (!PyArg_ParseTuple(args, "sO", &name, &valueObj)) return NULL; if (Options.count(name)) - Options[name] = string(PyBytes_AS_STRING(PyUnicode_AsEncodedString(PyObject_Str(valueObj), "UTF-8", "strict"))); + Options[name] = std::string(PyBytes_AS_STRING(PyUnicode_AsEncodedString(PyObject_Str(valueObj), "UTF-8", "strict"))); else { - PyErr_SetString(PyExc_ValueError, (string("No such option ")+name+"'").c_str()); + PyErr_SetString(PyExc_ValueError, (std::string("No such option ") + name + "'").c_str()); return NULL; } Py_RETURN_NONE; @@ -226,7 +361,7 @@ extern "C" PyObject* pyffish_startFen(PyObject* self, PyObject *args) { return NULL; } - return Py_BuildValue("s", variants.find(string(variant))->second->startFen.c_str()); + return Py_BuildValue("s", variants.find(std::string(variant))->second->startFen.c_str()); } // INPUT variant @@ -237,7 +372,7 @@ extern "C" PyObject* pyffish_twoBoards(PyObject* self, PyObject *args) { return NULL; } - return Py_BuildValue("O", variants.find(string(variant))->second->twoBoards ? Py_True : Py_False); + return Py_BuildValue("O", variants.find(std::string(variant))->second->twoBoards ? Py_True : Py_False); } // INPUT variant, fen, move @@ -247,15 +382,16 @@ extern "C" PyObject* pyffish_getSAN(PyObject* self, PyObject *args) { const char *fen, *variant, *move; int chess960 = false; - if (!PyArg_ParseTuple(args, "sss|p", &variant, &fen, &move, &chess960)) { + Notation notation = NOTATION_DEFAULT; + if (!PyArg_ParseTuple(args, "sss|pi", &variant, &fen, &move, &chess960, ¬ation)) { return NULL; } + if (notation == NOTATION_DEFAULT) + notation = default_notation(variants.find(std::string(variant))->second); StateListPtr states(new std::deque(1)); buildPosition(pos, states, variant, fen, moveList, chess960); - string moveStr = move; - const Variant* v = variants.find(string(variant))->second; - const bool shogi = v->variantTemplate == "shogi"; - return Py_BuildValue("s", move_to_san(pos, UCI::to_move(pos, moveStr), shogi).c_str()); + std::string moveStr = move; + return Py_BuildValue("s", move_to_san(pos, UCI::to_move(pos, moveStr), notation).c_str()); } // INPUT variant, fen, movelist @@ -265,23 +401,23 @@ extern "C" PyObject* pyffish_getSANmoves(PyObject* self, PyObject *args) { const char *fen, *variant; int chess960 = false; - if (!PyArg_ParseTuple(args, "ssO!|p", &variant, &fen, &PyList_Type, &moveList, &chess960)) { + Notation notation = NOTATION_DEFAULT; + if (!PyArg_ParseTuple(args, "ssO!|pi", &variant, &fen, &PyList_Type, &moveList, &chess960, ¬ation)) { return NULL; } + if (notation == NOTATION_DEFAULT) + notation = default_notation(variants.find(std::string(variant))->second); StateListPtr states(new std::deque(1)); buildPosition(pos, states, variant, fen, sanMoves, chess960); - const Variant* v = variants.find(string(variant))->second; - const bool shogi = v->variantTemplate == "shogi"; - int numMoves = PyList_Size(moveList); for (int i=0; i(pos)) { PyObject *moveStr; - moveStr=Py_BuildValue("s", UCI::move(pos, m).c_str()); + moveStr = Py_BuildValue("s", UCI::move(pos, m).c_str()); PyList_Append(legalMoves, moveStr); Py_XDECREF(moveStr); } @@ -467,11 +603,24 @@ PyMODINIT_FUNC PyInit_pyffish() { Py_INCREF(PyFFishError); PyModule_AddObject(module, "error", PyFFishError); + // values + PyModule_AddObject(module, "VALUE_MATE", PyLong_FromLong(VALUE_MATE)); + PyModule_AddObject(module, "VALUE_DRAW", PyLong_FromLong(VALUE_DRAW)); + + // notations + PyModule_AddObject(module, "NOTATION_DEFAULT", PyLong_FromLong(NOTATION_DEFAULT)); + PyModule_AddObject(module, "NOTATION_SAN", PyLong_FromLong(NOTATION_SAN)); + PyModule_AddObject(module, "NOTATION_LAN", PyLong_FromLong(NOTATION_LAN)); + PyModule_AddObject(module, "NOTATION_SHOGI_HOSKING", PyLong_FromLong(NOTATION_SHOGI_HOSKING)); + PyModule_AddObject(module, "NOTATION_SHOGI_HODGES", PyLong_FromLong(NOTATION_SHOGI_HODGES)); + PyModule_AddObject(module, "NOTATION_JANGGI", PyLong_FromLong(NOTATION_JANGGI)); + PyModule_AddObject(module, "NOTATION_XIANGQI_WXF", PyLong_FromLong(NOTATION_XIANGQI_WXF)); + // initialize stockfish pieceMap.init(); variants.init(); UCI::init(Options); - PSQT::init(variants.find("chess")->second); + PSQT::init(variants.find(Options["UCI_Variant"])->second); Bitboards::init(); Position::init(); Bitbases::init(); diff --git a/test.py b/test.py index bba06ad..6b6a93c 100644 --- a/test.py +++ b/test.py @@ -241,6 +241,9 @@ class TestPyffish(unittest.TestCase): result = sf.get_san("capablanca", CAPA, "e2e4") self.assertEqual(result, "e4") + result = sf.get_san("capablanca", CAPA, "e2e4", False, sf.NOTATION_LAN) + self.assertEqual(result, "e2-e4") + result = sf.get_san("capablanca", CAPA, "h1i3") self.assertEqual(result, "Ci3") @@ -258,39 +261,59 @@ class TestPyffish(unittest.TestCase): result = sf.get_san("shogi", SHOGI, "i3i4") self.assertEqual(result, "P-1f") - result = sf.get_san("shogi", SHOGI, "f1e2") + result = sf.get_san("shogi", SHOGI, "i3i4", False, sf.NOTATION_SHOGI_HOSKING) + self.assertEqual(result, "P16") + + result = sf.get_san("shogi", SHOGI, "f1e2", False, sf.NOTATION_SHOGI_HODGES) self.assertEqual(result, "G4i-5h") fen = "lnsgkgsnl/1r5b1/pppppp1pp/6p2/9/2P6/PP1PPPPPP/1B5R1/LNSGKGSNL w -" - result = sf.get_san("shogi", fen, "b2h8") + result = sf.get_san("shogi", fen, "b2h8", False, sf.NOTATION_SHOGI_HODGES) self.assertEqual(result, "Bx2b=") - result = sf.get_san("shogi", fen, "b2h8+") + result = sf.get_san("shogi", fen, "b2h8+", False, sf.NOTATION_SHOGI_HODGES) self.assertEqual(result, "Bx2b+") fen = "lnsgkg1nl/1r5s1/pppppp1pp/6p2/9/2P6/PP1PPPPPP/7R1/LNSGKGSNL[Bb] w " - result = sf.get_san("shogi", fen, "B@g7") + result = sf.get_san("shogi", fen, "B@g7", False, sf.NOTATION_SHOGI_HODGES) self.assertEqual(result, "B*3c") fen = "lnsgkg1nl/1r4s+B1/pppppp1pp/6p2/9/2P6/PP1PPPPPP/7R1/LNSGKGSNL[B] w " - result = sf.get_san("shogi", fen, "h8g7") + result = sf.get_san("shogi", fen, "h8g7", False, sf.NOTATION_SHOGI_HODGES) self.assertEqual(result, "+B-3c") fen = "lnk2gsnl/7b1/p1p+SGp1pp/6p2/1pP6/4P4/PP3PPPP/1S2G2R1/L2GK1bNL[PRppns] w " - result = sf.get_san("shogi", fen, "d7d8") + result = sf.get_san("shogi", fen, "d7d8", False, sf.NOTATION_SHOGI_HODGES) self.assertEqual(result, "+S-6b") result = sf.get_san("xiangqi", XIANGQI, "h1g3") self.assertEqual(result, "Hg3") + result = sf.get_san("xiangqi", XIANGQI, "h1g3", False, sf.NOTATION_XIANGQI_WXF) + self.assertEqual(result, "H2+3") + result = sf.get_san("xiangqi", XIANGQI, "c1e3") self.assertEqual(result, "Ece3") + result = sf.get_san("xiangqi", XIANGQI, "c1e3", False, sf.NOTATION_XIANGQI_WXF) + self.assertEqual(result, "E7+5") + result = sf.get_san("xiangqi", XIANGQI, "h3h10") self.assertEqual(result, "Cxh10") + result = sf.get_san("xiangqi", XIANGQI, "h3h10", False, sf.NOTATION_XIANGQI_WXF) + self.assertEqual(result, "C2+7") + result = sf.get_san("xiangqi", XIANGQI, "h3h5") self.assertEqual(result, "Ch5") + # Tandem pawns + fen = "rnbakabnr/9/1c5c1/p1p1P1p1p/4P4/9/P3P3P/1C5C1/9/RNBAKABNR w - - 0 1" + result = sf.get_san("xiangqi", fen, "e7d7", False, sf.NOTATION_XIANGQI_WXF) + self.assertEqual(result, "15=6") + + result = sf.get_san("janggi", JANGGI, "b1c3", False, sf.NOTATION_JANGGI) + self.assertEqual(result, "H02-83") + fen = "1rb1ka2r/4a4/2ncb1nc1/p1p1p1p1p/9/2P6/P3PNP1P/2N1C2C1/9/R1BAKAB1R w - - 1 7" result = sf.get_san("xiangqi", fen, "c3e2") self.assertEqual(result, "Hce2") @@ -342,6 +365,18 @@ class TestPyffish(unittest.TestCase): result = sf.get_san_moves("seirawan", SEIRAWAN, UCI_moves) self.assertEqual(result, SAN_moves) + UCI_moves = ["c3c4", "g7g6", "b2h8"] + SAN_moves = ["P-7f", "P-3d", "Bx2b="] + + result = sf.get_san_moves("shogi", SHOGI, UCI_moves) + self.assertEqual(result, SAN_moves) + + UCI_moves = ["h3e3", "h10g8", "h1g3", "c10e8", "a1a3", "i10h10"] + SAN_moves = ["C2=5", "H8+7", "H2+3", "E3+5", "R9+2", "R9=8"] + + result = sf.get_san_moves("xiangqi", XIANGQI, UCI_moves, False, sf.NOTATION_XIANGQI_WXF) + self.assertEqual(result, SAN_moves) + def test_gives_check(self): result = sf.gives_check("capablanca", CAPA, []) self.assertFalse(result) -- 1.7.0.4