Support more notation systems in pyffish
authorFabian Fichter <ianfab@users.noreply.github.com>
Sun, 5 Apr 2020 14:12:03 +0000 (16:12 +0200)
committerFabian Fichter <ianfab@users.noreply.github.com>
Sun, 5 Apr 2020 14:12:03 +0000 (16:12 +0200)
Add long algebraic notation and western notations for Shogi, Xiangqi, and Janggi.

- Shogi Hodges and Hosking
- Xiangqi WXF
- Janggi

src/pyffish.cpp
test.py

index 4625c69..9a45089 100644 (file)
@@ -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<PIECE_PROMOTION>(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<LEGAL>(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<PIECE_PROMOTION>(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<LEGAL>(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<StateInfo>(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; i<numMoves ; i++) {
-        string moveStr( PyBytes_AS_STRING(PyUnicode_AsEncodedString( PyList_GetItem(moveList, i), "UTF-8", "strict")) );
+    for (int i = 0; i < numMoves ; i++)
+    {
+        std::string moveStr(PyBytes_AS_STRING(PyUnicode_AsEncodedString( PyList_GetItem(moveList, i), "UTF-8", "strict")));
         Move m;
         if ((m = UCI::to_move(pos, moveStr)) != MOVE_NONE)
         {
@@ -189,9 +326,7 @@ void buildPosition(Position& pos, StateListPtr& states, const char *variant, con
             pos.do_move(m, states->back());
         }
         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, &notation)) {
         return NULL;
     }
+    if (notation == NOTATION_DEFAULT)
+        notation = default_notation(variants.find(std::string(variant))->second);
     StateListPtr states(new std::deque<StateInfo>(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, &notation)) {
         return NULL;
     }
+    if (notation == NOTATION_DEFAULT)
+        notation = default_notation(variants.find(std::string(variant))->second);
     StateListPtr states(new std::deque<StateInfo>(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<numMoves ; i++) {
-        string moveStr( PyBytes_AS_STRING(PyUnicode_AsEncodedString( PyList_GetItem(moveList, i), "UTF-8", "strict")) );
+        std::string moveStr(PyBytes_AS_STRING(PyUnicode_AsEncodedString( PyList_GetItem(moveList, i), "UTF-8", "strict")));
         Move m;
         if ((m = UCI::to_move(pos, moveStr)) != MOVE_NONE)
         {
             //add to the san move list
-            PyObject *move=Py_BuildValue("s", move_to_san(pos, m, shogi).c_str());
+            PyObject *move = Py_BuildValue("s", move_to_san(pos, m, notation).c_str());
             PyList_Append(sanMoves, move);
             Py_XDECREF(move);
 
@@ -291,7 +427,7 @@ extern "C" PyObject* pyffish_getSANmoves(PyObject* self, PyObject *args) {
         }
         else
         {
-            PyErr_SetString(PyExc_ValueError, (string("Invalid move '")+moveStr+"'").c_str());
+            PyErr_SetString(PyExc_ValueError, (std::string("Invalid move '") + moveStr + "'").c_str());
             return NULL;
         }
     }
@@ -314,7 +450,7 @@ extern "C" PyObject* pyffish_legalMoves(PyObject* self, PyObject *args) {
     for (const auto& m : MoveList<LEGAL>(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 (file)
--- 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)