Merge official-stockfish/master
authorFabian Fichter <ianfab@users.noreply.github.com>
Sun, 20 Dec 2020 21:55:14 +0000 (22:55 +0100)
committerFabian Fichter <ianfab@users.noreply.github.com>
Sun, 20 Dec 2020 21:55:14 +0000 (22:55 +0100)
bench: 4210065

1  2 
src/endgame.cpp
src/evaluate.cpp
src/movegen.cpp
src/pawns.cpp
src/position.cpp
src/position.h
test.py

diff --cc src/endgame.cpp
Simple merge
@@@ -449,15 -393,13 +448,16 @@@ namespace 
  
      attackedBy[Us][Pt] = 0;
  
-     for (Square s = *pl; s != SQ_NONE; s = *++pl)
-     {
+     while (b1) {
+         Square s = pop_lsb(&b1);
          // Find attacked squares, including x-ray attacks for bishops and rooks
          b = Pt == BISHOP ? attacks_bb<BISHOP>(s, pos.pieces() ^ pos.pieces(QUEEN))
 -          : Pt ==   ROOK ? attacks_bb<  ROOK>(s, pos.pieces() ^ pos.pieces(QUEEN) ^ pos.pieces(Us, ROOK))
 -                         : attacks_bb<Pt>(s, pos.pieces());
 +          : Pt ==   ROOK && !pos.diagonal_lines() ? attacks_bb<  ROOK>(s, pos.pieces() ^ pos.pieces(QUEEN) ^ pos.pieces(Us, ROOK))
 +                         : pos.attacks_from(Us, Pt, s);
 +
 +        // Restrict mobility to actual squares of board
 +        b &= pos.board_bb(Us, Pt);
  
          if (pos.blockers_for_king(Us) & s)
              b &= line_bb(pos.square<KING>(Us), s);
              }
          } // r > RANK_3
  
 -        score += bonus - PassedFile * edge_distance(file_of(s));
 +        score += bonus - PassedFile * edge_distance(file_of(s), pos.max_file());
 +    }
 +
 +    // Scale by maximum promotion piece value
 +    Value maxMg = VALUE_ZERO, maxEg = VALUE_ZERO;
 +    for (PieceType pt : pos.promotion_piece_types())
 +    {
 +        maxMg = std::max(maxMg, PieceValue[MG][pt]);
 +        maxEg = std::max(maxEg, PieceValue[EG][pt]);
 +    }
 +    score = make_score(mg_value(score) * int(maxMg - PawnValueMg) / (QueenValueMg - PawnValueMg),
 +                       eg_value(score) * int(maxEg - PawnValueEg) / (QueenValueEg - PawnValueEg));
 +
 +    // Score passed shogi pawns
-     const Square* pl = pos.squares(Us, SHOGI_PAWN);
-     Square s;
 +    PieceType pt = pos.promoted_piece_type(SHOGI_PAWN);
 +    if (pt != NO_PIECE_TYPE)
 +    {
-         while ((s = *pl++) != SQ_NONE)
++        b = pos.pieces(Us, SHOGI_PAWN);
++        while (b)
 +        {
++            Square s = pop_lsb(&b);
 +            if ((pos.pieces(Them, SHOGI_PAWN) & forward_file_bb(Us, s)) || relative_rank(Us, s, pos.max_rank()) == pos.max_rank())
 +                continue;
 +
 +            Square blockSq = s + Up;
 +            int d = std::max(pos.promotion_rank() - relative_rank(Us, s, pos.max_rank()), 1);
 +            d += !!(attackedBy[Them][ALL_PIECES] & ~attackedBy2[Us] & blockSq);
 +            score += make_score(PieceValue[MG][pt], PieceValue[EG][pt]) / (4 * d * d);
 +        }
      }
  
      if (T)
diff --cc src/movegen.cpp
@@@ -262,64 -175,33 +262,65 @@@ namespace 
    }
  
  
 -  template<Color Us, PieceType Pt, bool Checks>
 -  ExtMove* generate_moves(const Position& pos, ExtMove* moveList, Bitboard target) {
 +  template<Color Us, bool Checks>
 +  ExtMove* generate_moves(const Position& pos, ExtMove* moveList, PieceType pt, Bitboard target) {
  
 -    static_assert(Pt != KING && Pt != PAWN, "Unsupported piece type in generate_moves()");
 +    assert(pt != KING && pt != PAWN);
  
-     const Square* pl = pos.squares(Us, pt);
 -    Bitboard bb = pos.pieces(Us, Pt);
++    Bitboard bb = pos.pieces(Us, pt);
+     while (bb) {
+         Square from = pop_lsb(&bb);
  
-     for (Square from = *pl; from != SQ_NONE; from = *++pl)
-     {
 +        // Avoid generating discovered checks twice
 +        if (Checks && (pos.blockers_for_king(~Us) & from))
 +            continue;
 +
 +        Bitboard b1 = (  (pos.attacks_from(Us, pt, from) & pos.pieces())
 +                       | (pos.moves_from(Us, pt, from) & ~pos.pieces())) & target;
 +        PieceType prom_pt = pos.promoted_piece_type(pt);
 +        Bitboard b2 = prom_pt && (!pos.promotion_limit(prom_pt) || pos.promotion_limit(prom_pt) > pos.count(Us, prom_pt)) ? b1 : Bitboard(0);
 +        Bitboard b3 = pos.piece_demotion() && pos.is_promoted(from) ? b1 : Bitboard(0);
 +
          if (Checks)
          {
 -            if (    (Pt == BISHOP || Pt == ROOK || Pt == QUEEN)
 -                && !(attacks_bb<Pt>(from) & target & pos.check_squares(Pt)))
 -                continue;
 +            b1 &= pos.check_squares(pt);
 +            if (b2)
 +                b2 &= pos.check_squares(pos.promoted_piece_type(pt));
 +            if (b3)
 +                b3 &= pos.check_squares(type_of(pos.unpromoted_piece_on(from)));
 +        }
  
 -            if (pos.blockers_for_king(~Us) & from)
 -                continue;
 +        // Restrict target squares considering promotion zone
 +        if (b2 | b3)
 +        {
 +            Bitboard promotion_zone = zone_bb(Us, pos.promotion_rank(), pos.max_rank());
 +            if (pos.mandatory_piece_promotion())
 +                b1 &= (promotion_zone & from ? Bitboard(0) : ~promotion_zone) | (pos.piece_promotion_on_capture() ? ~pos.pieces() : Bitboard(0));
 +            // Exclude quiet promotions/demotions
 +            if (pos.piece_promotion_on_capture())
 +            {
 +                b2 &= pos.pieces();
 +                b3 &= pos.pieces();
 +            }
 +            // Consider promotions/demotions into promotion zone
 +            if (!(promotion_zone & from))
 +            {
 +                b2 &= promotion_zone;
 +                b3 &= promotion_zone;
 +            }
          }
  
 -        Bitboard b = attacks_bb<Pt>(from, pos.pieces()) & target;
 +        while (b1)
 +            moveList = make_move_and_gating<NORMAL>(pos, moveList, Us, from, pop_lsb(&b1));
  
 -        if (Checks)
 -            b &= pos.check_squares(Pt);
 +        // Shogi-style piece promotions
 +        while (b2)
 +            *moveList++ = make<PIECE_PROMOTION>(from, pop_lsb(&b2));
  
 -        while (b)
 -            *moveList++ = make_move(from, pop_lsb(&b));
 +        // Piece demotions
 +        while (b3)
 +            *moveList++ = make<PIECE_DEMOTION>(from, pop_lsb(&b3));
      }
  
      return moveList;
diff --cc src/pawns.cpp
@@@ -109,11 -104,12 +109,12 @@@ namespace 
      e->blockedCount += popcount(shift<Up>(ourPawns) & (theirPawns | doubleAttackThem));
  
      // Loop through all pawns of the current color and score each pawn
-     while ((s = *pl++) != SQ_NONE)
-     {
+     while (b) {
+         s = pop_lsb(&b);
          assert(pos.piece_on(s) == make_piece(Us, PAWN));
  
 -        Rank r = relative_rank(Us, s);
 +        Rank r = relative_rank(Us, s, pos.max_rank());
  
          // Flag the pawn
          opposed    = theirPawns & forward_file_bb(Us, s);
@@@ -242,18 -192,15 +242,18 @@@ Position& Position::set(const Variant* 
  
    unsigned char col, row, token;
    size_t idx;
 -  Square sq = SQ_A8;
    std::istringstream ss(fenStr);
  
-   std::memset(static_cast<void*>(this), 0, sizeof(Position));
-   std::memset(static_cast<void*>(si), 0, sizeof(StateInfo));
-   std::fill_n(&pieceList[0][0], sizeof(pieceList) / sizeof(Square), SQ_NONE);
-   var = v;
+   std::memset(this, 0, sizeof(Position));
+   std::memset(si, 0, sizeof(StateInfo));
    st = si;
  
++  var = v;
++
    ss >> std::noskipws;
  
 +  Square sq = SQ_A1 + max_rank() * NORTH;
 +
    // 1. Piece placement
    while ((ss >> token) && !isspace(token))
    {
@@@ -2376,18 -1325,10 +2376,14 @@@ bool Position::pos_is_ok() const 
    if (std::memcmp(&si, st, sizeof(StateInfo)))
        assert(0 && "pos_is_ok: State");
  
 -  for (Piece pc : Pieces)
 -      if (   pieceCount[pc] != popcount(pieces(color_of(pc), type_of(pc)))
 -          || pieceCount[pc] != std::count(board, board + SQUARE_NB, pc))
 -          assert(0 && "pos_is_ok: Pieces");
 +  for (Color c : {WHITE, BLACK})
 +      for (PieceType pt = PAWN; pt <= KING; ++pt)
 +      {
 +          Piece pc = make_piece(c, pt);
 +          if (   pieceCount[pc] != popcount(pieces(c, pt))
 +              || pieceCount[pc] != std::count(board, board + SQUARE_NB, pc))
 +              assert(0 && "pos_is_ok: Pieces");
-           for (int i = 0; i < pieceCount[pc]; ++i)
-               if (board[pieceList[pc][i]] != pc || index[pieceList[pc][i]] != i)
-                   assert(0 && "pos_is_ok: Index");
 +      }
  
    for (Color c : { WHITE, BLACK })
        for (CastlingRights cr : {c & KING_SIDE, c & QUEEN_SIDE})
diff --cc src/position.h
@@@ -191,18 -94,11 +191,16 @@@ public
    Bitboard pieces(Color c) const;
    Bitboard pieces(Color c, PieceType pt) const;
    Bitboard pieces(Color c, PieceType pt1, PieceType pt2) const;
 +  Bitboard major_pieces(Color c) const;
 +  Bitboard non_sliding_riders() const;
    Piece piece_on(Square s) const;
 +  Piece unpromoted_piece_on(Square s) const;
    Square ep_square() const;
 +  Bitboard gates(Color c) const;
    bool empty(Square s) const;
 +  int count(Color c, PieceType pt) const;
    template<PieceType Pt> int count(Color c) const;
    template<PieceType Pt> int count() const;
-   template<PieceType Pt> const Square* squares(Color c) const;
-   const Square* squares(Color c, PieceType pt) const;
    template<PieceType Pt> Square square(Color c) const;
    bool is_on_semiopen_file(Color c, Square s) const;
  
@@@ -1096,13 -387,9 +1084,12 @@@ inline void Position::put_piece(Piece p
    board[s] = pc;
    byTypeBB[ALL_PIECES] |= byTypeBB[type_of(pc)] |= s;
    byColorBB[color_of(pc)] |= s;
-   index[s] = pieceCount[pc]++;
-   pieceList[pc][index[s]] = s;
+   pieceCount[pc]++;
    pieceCount[make_piece(color_of(pc), ALL_PIECES)]++;
    psq += PSQT::psq[pc][s];
 +  if (isPromoted)
 +      promotedPieces |= s;
 +  unpromotedBoard[s] = unpromotedPc;
  }
  
  inline void Position::remove_piece(Square s) {
    byTypeBB[type_of(pc)] ^= s;
    byColorBB[color_of(pc)] ^= s;
    /* board[s] = NO_PIECE;  Not needed, overwritten by the capturing one */
-   Square lastSquare = pieceList[pc][--pieceCount[pc]];
-   index[lastSquare] = index[s];
-   pieceList[pc][index[lastSquare]] = lastSquare;
-   pieceList[pc][pieceCount[pc]] = SQ_NONE;
+   pieceCount[pc]--;
    pieceCount[make_piece(color_of(pc), ALL_PIECES)]--;
    psq -= PSQT::psq[pc][s];
 +  promotedPieces -= s;
 +  unpromotedBoard[s] = NO_PIECE;
  }
  
  inline void Position::move_piece(Square from, Square to) {
  
-   // index[from] is not updated and becomes stale. This works as long as index[]
-   // is accessed just by known occupied squares.
    Piece pc = board[from];
 -  Bitboard fromTo = from | to;
 +  Bitboard fromTo = square_bb(from) ^ to; // from == to needs to cancel out
    byTypeBB[ALL_PIECES] ^= fromTo;
    byTypeBB[type_of(pc)] ^= fromTo;
    byColorBB[color_of(pc)] ^= fromTo;
    board[from] = NO_PIECE;
    board[to] = pc;
-   index[to] = index[from];
-   pieceList[pc][index[to]] = to;
    psq += PSQT::psq[pc][to] - PSQT::psq[pc][from];
 +  if (is_promoted(from))
 +      promotedPieces ^= fromTo;
 +  unpromotedBoard[to] = unpromotedBoard[from];
 +  unpromotedBoard[from] = NO_PIECE;
  }
  
  inline void Position::do_move(Move m, StateInfo& newSt) {
diff --cc test.py
index ecc793a,0000000..59fae4a
mode 100644,000000..100644
--- /dev/null
+++ b/test.py
@@@ -1,510 -1,0 +1,510 @@@
 +# -*- coding: utf-8 -*-
 +
 +import faulthandler
 +import unittest
 +import pyffish as sf
 +
 +faulthandler.enable()
 +
 +CHESS = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
 +CHESS960 = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w HAha - 0 1"
 +CAPA = "rnabqkbcnr/pppppppppp/10/10/10/10/PPPPPPPPPP/RNABQKBCNR w KQkq - 0 1"
 +CAPAHOUSE = "rnabqkbcnr/pppppppppp/10/10/10/10/PPPPPPPPPP/RNABQKBCNR[] w KQkq - 0 1"
 +SITTUYIN = "8/8/4pppp/pppp4/4PPPP/PPPP4/8/8[KFRRSSNNkfrrssnn] w - - 0 1"
 +MAKRUK = "rnsmksnr/8/pppppppp/8/8/PPPPPPPP/8/RNSKMSNR w - - 0 1"
 +SHOGI = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL[-] w - - 0 1"
 +SHOGI_SFEN = "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1"
 +SEIRAWAN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[HEhe] w KQBCDFGkqbcdfg - 0 1"
 +GRAND = "r8r/1nbqkcabn1/pppppppppp/10/10/10/10/PPPPPPPPPP/1NBQKCABN1/R8R w - - 0 1"
 +GRANDHOUSE = "r8r/1nbqkcabn1/pppppppppp/10/10/10/10/PPPPPPPPPP/1NBQKCABN1/R8R[] w - - 0 1"
 +XIANGQI = "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1"
 +SHOGUN = "rnb+fkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNB+FKBNR w KQkq - 0 1"
 +JANGGI = "rnba1abnr/4k4/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/4K4/RNBA1ABNR w - - 0 1"
 +
 +
 +ini_text = """
 +# Hybrid variant of Grand-chess and crazyhouse, using Grand-chess as a template
 +[grandhouse:grand]
 +startFen = r8r/1nbqkcabn1/pppppppppp/10/10/10/10/PPPPPPPPPP/1NBQKCABN1/R8R[] w - - 0 1
 +pieceDrops = true
 +capturesToHand = true
 +
 +# Shogun chess
 +[shogun:crazyhouse]
 +startFen = rnb+fkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNB+FKBNR w KQkq - 0 1
 +commoner = c
 +centaur = g
 +archbishop = a
 +chancellor = m
 +fers = f
 +promotionRank = 6
 +promotionLimit = g:1 a:1 m:1 q:1
 +promotionPieceTypes = -
 +promotedPieceType = p:c n:g b:a r:m f:q
 +mandatoryPawnPromotion = false
 +firstRankPawnDrops = true
 +promotionZonePawnDrops = true
 +whiteDropRegion = *1 *2 *3 *4 *5
 +blackDropRegion = *4 *5 *6 *7 *8
 +immobilityIllegal = true
 +
 +# Asymmetric variant with one army using pieces that move like knights but attack like other pieces (kniroo and knibis)
 +[orda:chess]
 +startFen = lhaykahl/8/pppppppp/8/8/8/PPPPPPPP/RNBQKBNR w KQ - 0 1
 +centaur = h
 +knibis = a
 +kniroo = l
 +silver = y
 +promotionPieceTypes = qh
 +flagPiece = k
 +whiteFlag = *8
 +blackFlag = *1
 +"""
 +
 +sf.load_variant_config(ini_text)
 +
 +
 +variant_positions = {
 +    "chess": {
 +        "k7/8/8/8/8/8/8/K7 w - - 0 1": (True, True),  # K vs K
 +        "k7/b7/8/8/8/8/8/K7 w - - 0 1": (True, True),  # K vs KB
 +        "k7/n7/8/8/8/8/8/K7 w - - 0 1": (True, True),  # K vs KN
 +        "k7/p7/8/8/8/8/8/K7 w - - 0 1": (True, False),  # K vs KP
 +        "k7/r7/8/8/8/8/8/K7 w - - 0 1": (True, False),  # K vs KR
 +        "k7/q7/8/8/8/8/8/K7 w - - 0 1": (True, False),  # K vs KQ
 +        "k7/nn6/8/8/8/8/8/K7 w - - 0 1": (True, False),  # K vsNN K
 +        "k7/bb6/8/8/8/8/8/K7 w - - 0 1": (True, False),  # K vs KBB opp color
 +        "k7/b1b5/8/8/8/8/8/K7 w - - 0 1": (True, True),  # K vs KBB same color
 +        "kb6/8/8/8/8/8/8/K1B6 w - - 0 1": (True, True),  # KB vs KB same color
 +        "kb6/8/8/8/8/8/8/KB7 w - - 0 1": (False, False),  # KB vs KB opp color
 +        "8/8/8/8/8/6KN/8/6nk w - - 0 1": (False, False),  # KN vs KN
 +    },
 +    "seirawan": {
 +        "k7/8/8/8/8/8/8/K7[] w - - 0 1": (True, True),  # K vs K
 +        "k7/8/8/8/8/8/8/KH6[] w - - 0 1": (False, True),  # KH vs K
 +        "k7/8/8/8/8/8/8/4K3[E] w E - 0 1": (False, True),  # KE vs K
 +    },
 +    "sittuyin": {
 +        "8/8/4pppp/pppp4/4PPPP/PPPP4/8/8[KFRRSSNNkfrrssnn] w - - 0 1": (False, False),  # starting position
 +        "k7/8/8/8/8/8/8/K7[] w - - 0 1": (True, True),  # K vs K
 +        "k6P/8/8/8/8/8/8/K7[] w - - 0 1": (True, True),  # KP vs K
 +        "k6P/8/8/8/8/8/8/K6p[] w - - 0 1": (False, False),  # KP vs KP
 +        "k7/8/8/8/8/8/8/KFF5[] w - - 0 1": (False, True),  # KFF vs K
 +        "k7/8/8/8/8/8/8/KS6[] w - - 0 1": (False, True),  # KS vs K
 +    },
 +    "xiangqi": {
 +        "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1": (False, False),  # starting position
 +        "5k3/4a4/3CN4/9/1PP5p/9/8P/4C4/4A4/2B1K4 w - - 0 46": (False, False),  # issue #53
 +        "4k4/9/9/9/9/9/9/9/9/4K4 w - - 0 1": (True, True),  # K vs K
 +        "4k4/9/9/4p4/9/9/9/9/9/4KR3 w - - 0 1": (False, False),  # KR vs KP
 +        "4k4/9/9/9/9/9/9/9/9/3KN4 w - - 0 1": (False, True),  # KN vs K
 +        "4k4/9/4b4/9/9/9/9/4B4/9/4K4 w - - 0 1": (True, True),  # KB vs KB
 +        "4k4/9/9/9/9/9/9/9/4A4/4KC3 w - - 0 1": (False, True),  # KCA vs K
 +    },
 +    "janggi": {
 +        JANGGI: (False, False),  # starting position
 +        "5k3/4a4/3CN4/9/1PP5p/9/8P/4C4/4A4/2B1K4 w - - 0 46": (False, False),  # issue #53
 +        "4k4/9/9/9/9/4B4/4B4/9/9/4K4 w - - 0 1": (False, True),  # KEE vs K
 +        "4k4/9/9/9/9/9/9/9/4A4/4KC3 w - - 0 1": (False, True),  # KCA vs K
 +    },
 +    "shako": {
 +        "k9/10/10/10/10/10/10/10/10/KC8 w - - 0 1": (True, True),  # KC vs K
 +        "k9/10/10/10/10/10/10/10/10/KCC7 w - - 0 1": (False, True),  # KCC vs K
 +        "k9/10/10/10/10/10/10/10/10/KEC7 w - - 0 1": (False, True),  # KEC vs K
 +        "k9/10/10/10/10/10/10/10/10/KNE7 w - - 0 1": (False, True),  # KNE vs K
 +        "kb8/10/10/10/10/10/10/10/10/KE8 w - - 0 1": (False, False),  # KE vs KB opp color
 +        "kb8/10/10/10/10/10/10/10/10/K1E7 w - - 0 1": (True, True),  # KE vs KB same color
 +    },
 +    "orda": {
 +        "k7/8/8/8/8/8/8/K7 w - - 0 1": (False, False),  # K vs K
 +    },
 +}
 +
 +
 +class TestPyffish(unittest.TestCase):
 +    def test_version(self):
 +        result = sf.version()
 +        self.assertEqual(result, (0, 0, 51))
 +
 +    def test_info(self):
 +        result = sf.info()
 +        self.assertTrue(result.startswith("Fairy-Stockfish"))
 +
 +    def test_set_option(self):
 +        result = sf.set_option("UCI_Variant", "capablanca")
 +        self.assertIsNone(result)
 +
 +    def test_two_boards(self):
 +        self.assertFalse(sf.two_boards("chess"))
 +        self.assertTrue(sf.two_boards("bughouse"))
 +
 +    def test_start_fen(self):
 +        result = sf.start_fen("capablanca")
 +        self.assertEqual(result, CAPA)
 +
 +        result = sf.start_fen("capahouse")
 +        self.assertEqual(result, CAPAHOUSE)
 +
 +        result = sf.start_fen("xiangqi")
 +        self.assertEqual(result, XIANGQI)
 +
 +        result = sf.start_fen("grandhouse")
 +        self.assertEqual(result, GRANDHOUSE)
 +
 +        result = sf.start_fen("shogun")
 +        self.assertEqual(result, SHOGUN)
 +
 +    def test_legal_moves(self):
 +        fen = "10/10/10/10/10/k9/10/K9 w - - 0 1"
 +        result = sf.legal_moves("capablanca", fen, [])
 +        self.assertEqual(result, ["a1b1"])
 +
 +        result = sf.legal_moves("grand", GRAND, ["a3a5"])
 +        self.assertIn("a10b10", result)
 +
 +        result = sf.legal_moves("xiangqi", XIANGQI, ["h3h10"])
 +        self.assertIn("i10h10", result)
 +
 +        result = sf.legal_moves("xiangqi", XIANGQI, ["h3h10"])
 +        self.assertIn("i10h10", result)
 +
 +        result = sf.legal_moves("shogun", SHOGUN, ["c2c4", "b8c6", "b2b4", "b7b5", "c4b5", "c6b8"])
 +        self.assertIn("b5b6+", result)
 +
 +        # In Janggi stalemate position pass move (in place king move) is possible
 +        fen = "4k4/c7R/9/3R1R3/9/9/9/9/9/3K5 b - - 0 1"
 +        result = sf.legal_moves("janggi", fen, [])
 +        self.assertEqual(result, ["e10e10"])
 +
 +    def test_short_castling(self):
 +        legals = ['f5f4', 'a7a6', 'b7b6', 'c7c6', 'd7d6', 'e7e6', 'i7i6', 'j7j6', 'a7a5', 'b7b5', 'c7c5', 'e7e5', 'i7i5', 'j7j5', 'b8a6', 'b8c6', 'h6g4', 'h6i4', 'h6j5', 'h6f7', 'h6g8', 'h6i8', 'd5a2', 'd5b3', 'd5f3', 'd5c4', 'd5e4', 'd5c6', 'd5e6', 'd5f7', 'd5g8', 'j8g8', 'j8h8', 'j8i8', 'e8f7', 'c8b6', 'c8d6', 'g6g2', 'g6g3', 'g6f4', 'g6g4', 'g6h4', 'g6e5', 'g6g5', 'g6i5', 'g6a6', 'g6b6', 'g6c6', 'g6d6', 'g6e6', 'g6f6', 'g6h8', 'f8f7', 'f8g8', 'f8i8']
 +        moves = ['b2b4', 'f7f5', 'c2c3', 'g8d5', 'a2a4', 'h8g6', 'f2f3', 'i8h6', 'h2h3']
 +        result = sf.legal_moves("capablanca", CAPA, moves)
-         self.assertEqual(legals, result)
++        self.assertCountEqual(legals, result)
 +        self.assertIn("f8i8", result)
 +
 +        moves = ['a2a4', 'f7f5', 'b2b3', 'g8d5', 'b1a3', 'i8h6', 'c1a2', 'h8g6', 'c2c4']
 +        result = sf.legal_moves("capablanca", CAPA, moves)
 +        self.assertIn("f8i8", result)
 +
 +        moves = ['f2f4', 'g7g6', 'g1d4', 'j7j6', 'h1g3', 'b8a6', 'i1h3', 'h7h6']
 +        result = sf.legal_moves("capablanca", CAPA, moves)
 +        self.assertIn("f1i1", result)
 +
 +    def test_get_fen(self):
 +        result = sf.get_fen("chess", CHESS, [])
 +        self.assertEqual(result, CHESS)
 +
 +        result = sf.get_fen("capablanca", CAPA, [])
 +        self.assertEqual(result, CAPA)
 +
 +        result = sf.get_fen("xiangqi", XIANGQI, [])
 +        self.assertEqual(result, XIANGQI)
 +
 +        fen = "rnab1kbcnr/ppppPppppp/10/4q5/10/10/PPPPP1PPPP/RNABQKBCNR[p] b KQkq - 0 3"
 +        result = sf.get_fen("capahouse", CAPA, ["f2f4", "e7e5", "f4e5", "e8e5", "P@e7"])
 +        self.assertEqual(result, fen)
 +
 +        fen0 = "reb1k2r/ppppqppp/2nb1n2/4p3/4P3/N1P2N2/PB1PQPPP/RE2KBHR[h] b KQkqac - 2 6"
 +        fen1 = "reb2rk1/ppppqppp/2nb1n2/4p3/4P3/N1P2N2/PB1PQPPP/RE2KBHR[h] w KQac - 3 7"
 +        result = sf.get_fen("seirawan", fen0, ["e8g8"])
 +        self.assertEqual(result, fen1)
 +
 +        result = sf.get_fen("chess", CHESS, [], True, False, False)
 +        self.assertEqual(result, CHESS960)
 +
 +        # test O-O-O
 +        fen = "rbkqnrbn/pppppppp/8/8/8/8/PPPPPPPP/RBKQNRBN w AFaf - 0 1"
 +        moves = ["d2d4", "f7f5", "e1f3", "h8g6", "h1g3", "c7c6", "c2c3", "e7e6", "b1d3", "d7d5", "d1c2", "b8d6", "e2e3", "d8d7", "c1a1"]
 +        result = sf.get_fen("chess", fen, moves, True, False, False)
 +        self.assertEqual(result, "r1k1nrb1/pp1q2pp/2pbp1n1/3p1p2/3P4/2PBPNN1/PPQ2PPP/2KR1RB1 b fa - 2 8", CHESS960)
 +
 +        # SFEN
 +        result = sf.get_fen("shogi", SHOGI, [], False, True)
 +        self.assertEqual(result, SHOGI_SFEN)
 +
 +        # makruk FEN
 +        fen = "rnsmksnr/8/1pM~1pppp/p7/8/PPPP1PPP/8/RNSKMSNR b - - 0 3"
 +        result = sf.get_fen("makruk", MAKRUK, ["e3e4", "d6d5", "e4d5", "a6a5", "d5c6m"], False, False, True)
 +        self.assertEqual(result, fen)
 +        result = sf.get_fen("makruk", fen, [], False, False, True)
 +        self.assertEqual(result, fen)
 +
 +        # makruk piece honor counting
 +        fen = "8/3k4/8/2K1S1P1/8/8/8/8 w - - 0 1"
 +        moves = ["g5g6m"]
 +        result = sf.get_fen("makruk", fen, moves, False, False, True)
 +        self.assertEqual(result, "8/3k4/6M~1/2K1S3/8/8/8/8 b - 88 8 1")
 +
 +        fen = "8/2K3k1/5m2/4S1S1/8/8/8/8 w - 128 97 1"
 +        moves = ["e5f6"]
 +        result = sf.get_fen("makruk", fen, moves, False, False, True)
 +        self.assertEqual(result, "8/2K3k1/5S2/6S1/8/8/8/8 b - 44 8 1")
 +
 +        # makruk board honor counting
 +        fen = "3k4/2m5/8/4MP2/3KS3/8/8/8 w - - 0 1"
 +        moves = ["f5f6m"]
 +        result = sf.get_fen("makruk", fen, moves, False, False, True)
 +        self.assertEqual(result, "3k4/2m5/5M~2/4M3/3KS3/8/8/8 b - 128 0 1")
 +
 +        fen = "3k4/2m5/5M~2/4M3/3KS3/8/8/8 w - 128 0 33"
 +        moves = ["d4d5"]
 +        result = sf.get_fen("makruk", fen, moves, False, False, True)
 +        self.assertEqual(result, "3k4/2m5/5M~2/3KM3/4S3/8/8/8 b - 128 1 33")
 +
 +        fen = "3k4/2m5/5M~2/4M3/3KS3/8/8/8 w - 128 36 1"
 +        moves = ["d4d5"]
 +        result = sf.get_fen("makruk", fen, moves, False, False, True)
 +        self.assertEqual(result, "3k4/2m5/5M~2/3KM3/4S3/8/8/8 b - 128 37 1")
 +
 +        fen = "3k4/2m5/5M~2/4M3/3KS3/8/8/8 w - 128 0 33"
 +        moves = ["d4d5"]
 +        result = sf.get_fen("makruk", fen, moves, False, False, True, -1)
 +        self.assertEqual(result, "3k4/2m5/5M~2/3KM3/4S3/8/8/8 b - 128 0 33")
 +
 +        fen = "3k4/2m5/5M~2/4M3/3KS3/8/8/8 w - 128 7 33"
 +        moves = ["d4d5"]
 +        result = sf.get_fen("makruk", fen, moves, False, False, True, 58)
 +        self.assertEqual(result, "3k4/2m5/5M~2/3KM3/4S3/8/8/8 b - 128 8 33")
 +
 +    def test_get_san(self):
 +        fen = "4k3/8/3R4/8/1R3R2/8/3R4/4K3 w - - 0 1"
 +        result = sf.get_san("chess", fen, "b4d4")
 +        self.assertEqual(result, "Rbd4")
 +
 +        result = sf.get_san("chess", fen, "f4d4")
 +        self.assertEqual(result, "Rfd4")
 +
 +        result = sf.get_san("chess", fen, "d2d4")
 +        self.assertEqual(result, "R2d4")
 +
 +        result = sf.get_san("chess", fen, "d6d4")
 +        self.assertEqual(result, "R6d4")
 +
 +        fen = "4k3/8/3R4/3P4/1RP1PR2/8/3R4/4K3 w - - 0 1"
 +        result = sf.get_san("chess", fen, "d2d4")
 +        self.assertEqual(result, "Rd4")
 +
 +        fen = "1r2k3/P1P5/8/8/8/8/8/4K3 w - - 0 1"
 +        result = sf.get_san("chess", fen, "c7b8q")
 +        self.assertEqual(result, "cxb8=Q+")
 +
 +        fen = "1r2k3/P1P5/8/8/8/8/8/4K3 w - - 0 1"
 +        result = sf.get_san("chess", fen, "c7b8q", False, sf.NOTATION_LAN)
 +        self.assertEqual(result, "c7xb8=Q+")
 +
 +        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")
 +
 +        result = sf.get_san("sittuyin", SITTUYIN, "R@a1")
 +        self.assertEqual(result, "R@a1")
 +
 +        fen = "3rr3/1kn3n1/1ss1p1pp/1pPpP3/6PP/p3KN2/2SSFN2/3R3R[] b - - 0 14"
 +        result = sf.get_san("sittuyin", fen, "c6c5")
 +        self.assertEqual(result, "Scxc5")
 +
 +        fen = "7R/1r6/3k1np1/3s2N1/3s3P/4n3/6p1/2R3K1[] w - - 2 55"
 +        result = sf.get_san("sittuyin", fen, "h4h4f")
 +        self.assertEqual(result, "h4=F")
 +
 +        fen = "k7/2K3P1/8/4P3/8/8/8/1R6[] w - - 0 1"
 +        result = sf.get_san("sittuyin", fen, "e5f6f")
 +        self.assertEqual(result, "e5f6=F")
 +
 +        result = sf.get_san("shogi", SHOGI, "i3i4")
 +        self.assertEqual(result, "P-16")
 +
 +        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_HOSKING)
 +        self.assertEqual(result, "G49-58")
 +        result = sf.get_san("shogi", SHOGI, "f1e2", False, sf.NOTATION_SHOGI_HODGES)
 +        self.assertEqual(result, "G4i-5h")
 +        result = sf.get_san("shogi", SHOGI, "f1e2", False, sf.NOTATION_SHOGI_HODGES_NUMBER)
 +        self.assertEqual(result, "G49-58")
 +
 +        fen = "lnsgkgsnl/1r5b1/pppppp1pp/6p2/9/2P6/PP1PPPPPP/1B5R1/LNSGKGSNL w -"
 +        result = sf.get_san("shogi", fen, "b2h8", False, sf.NOTATION_SHOGI_HODGES)
 +        self.assertEqual(result, "Bx2b=")
 +        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", False, sf.NOTATION_SHOGI_HODGES)
 +        self.assertEqual(result, "B*3c")
 +        result = sf.get_san("shogi", fen, "B@g7", False, sf.NOTATION_SHOGI_HODGES_NUMBER)
 +        self.assertEqual(result, "B*33")
 +
 +        fen = "lnsgkg1nl/1r4s+B1/pppppp1pp/6p2/9/2P6/PP1PPPPPP/7R1/LNSGKGSNL[B] w "
 +        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", 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")
 +
 +        # skip disambiguation for elephants and advisors, but not for pieces that require it
 +        fen = "rnbakabnr/9/1c5c1/p1p1p1p1p/9/1NB6/P1P1P1P1P/1C1A3C1/9/RNBAK21R w - - 0 1"
 +        result = sf.get_san("xiangqi", fen, "c5e3", False, sf.NOTATION_XIANGQI_WXF)
 +        self.assertEqual(result, "E7-5")
 +        result = sf.get_san("xiangqi", fen, "d1e2", False, sf.NOTATION_XIANGQI_WXF)
 +        self.assertEqual(result, "A6+5")
 +        result = sf.get_san("xiangqi", fen, "b5c7", False, sf.NOTATION_XIANGQI_WXF)
 +        self.assertEqual(result, "H++7")
 +
 +        # 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")
 +
 +        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")
 +
 +        result = sf.get_san("xiangqi", fen, "c3d5")
 +        self.assertEqual(result, "Hd5")
 +
 +        result = sf.get_san("janggi", JANGGI, "b1c3", False, sf.NOTATION_JANGGI)
 +        self.assertEqual(result, "H02-83")
 +
 +        fen = "1b1aa2b1/5k3/3ncn3/1pp1pp3/5r2p/9/P1PPB1PPB/2N1CCN1c/9/R2AKAR2 w - - 19 17"
 +        result = sf.get_san("janggi", fen, "d1e2", False, sf.NOTATION_SAN)
 +        self.assertEqual(result, "Ade2")
 +
 +        fen = "1Pbcka3/3nNn1c1/N2CaC3/1pB6/9/9/5P3/9/4K4/9 w - - 0 23"
 +        result = sf.get_san("janggi", fen, "f8f10", False, sf.NOTATION_SAN)
 +        self.assertEqual(result, "Cfxf10")
 +
 +        fen = "rnsm1s1r/4n1k1/1ppppppp/p7/2PPP3/PP3PPP/4N2R/RNSKMS2 b - - 1 5"
 +        result = sf.get_san("makruk", fen, "f8f7")
 +        self.assertEqual(result, "Sf7")
 +
 +        fen = "4k3/8/8/4S3/8/2S5/8/4K3 w - - 0 1"
 +        result = sf.get_san("makruk", fen, "e5d4")
 +        self.assertEqual(result, "Sed4")
 +
 +        result = sf.get_san("makruk", fen, "c3d4")
 +        self.assertEqual(result, "Scd4")
 +
 +        fen = "4k3/8/8/3S4/8/3S4/8/4K3 w - - 0 1"
 +        result = sf.get_san("makruk", fen, "d3d4")
 +        self.assertEqual(result, "Sd4")
 +
 +        UCI_moves = ["e2e4", "e7e5", "g1f3", "b8c6h", "f1c4", "f8c5e"]
 +        SAN_moves = ["e4", "e5", "Nf3", "Nc6/H", "Bc4", "Bc5/E"]
 +
 +        fen = SEIRAWAN
 +        for i, move in enumerate(UCI_moves):
 +            result = sf.get_san("seirawan", fen, move)
 +            self.assertEqual(result, SAN_moves[i])
 +            fen = sf.get_fen("seirawan", SEIRAWAN, UCI_moves[:i + 1])
 +
 +        result = sf.get_san("seirawan", fen, "e1g1")
 +        self.assertEqual(result, "O-O")
 +
 +        result = sf.get_san("seirawan", fen, "e1g1h")
 +        self.assertEqual(result, "O-O/He1")
 +        result = sf.get_san("seirawan", fen, "e1g1e")
 +        self.assertEqual(result, "O-O/Ee1")
 +
 +        result = sf.get_san("seirawan", fen, "h1e1h")
 +        self.assertEqual(result, "O-O/Hh1")
 +        result = sf.get_san("seirawan", fen, "h1e1e")
 +        self.assertEqual(result, "O-O/Eh1")
 +
 +    def test_get_san_moves(self):
 +        UCI_moves = ["e2e4", "e7e5", "g1f3", "b8c6h", "f1c4", "f8c5e"]
 +        SAN_moves = ["e4", "e5", "Nf3", "Nc6/H", "Bc4", "Bc5/E"]
 +        result = sf.get_san_moves("seirawan", SEIRAWAN, UCI_moves)
 +        self.assertEqual(result, SAN_moves)
 +
 +        UCI_moves = ["c3c4", "g7g6", "b2h8"]
 +        SAN_moves = ["P-76", "P-34", "Bx22="]
 +        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)
 +
 +        UCI_moves = ["e2e4", "d7d5", "f1a6+", "d8d6"]
 +        SAN_moves = ["e4", "d5", "Ba6=A", "Qd6"]
 +        result = sf.get_san_moves("shogun", SHOGUN, UCI_moves)
 +        self.assertEqual(result, SAN_moves)
 +
 +    def test_gives_check(self):
 +        result = sf.gives_check("capablanca", CAPA, [])
 +        self.assertFalse(result)
 +
 +        result = sf.gives_check("capablanca", CAPA, ["e2e4"])
 +        self.assertFalse(result)
 +
 +        moves = ["g2g3", "d7d5", "a2a3", "c8h3"]
 +        result = sf.gives_check("capablanca", CAPA, moves)
 +        self.assertTrue(result)
 +
 +    def test_game_result(self):
 +        result = sf.game_result("chess", CHESS, ["f2f3", "e7e5", "g2g4", "d8h4"])
 +        self.assertTrue(result < 0)
 +
 +    def test_is_immediate_game_end(self):
 +        result = sf.is_immediate_game_end("capablanca", CAPA, [])
 +        self.assertFalse(result[0])
 +
 +        # bikjang (facing kings)
 +        moves = "e2e3 e9f9 h3d3 e7f7 i1i3 h10i8 i3h3 c10e7 h3h8 i10i9 h8b8 i9g9 d3f3 f9e9 f3f10 e7c10 f10c10 b10c8 c10g10 g9f9 b8c8 a10b10 b3f3 f9h9 a1a2 h9f9 a2d2 b10b9 d2d10 e9d10 c8c10 d10d9 f3f9 i8g9 f9b9 a7a6 g10g7 f7f6 e4e5 c7d7 g1e4 i7i6 e4b6 d9d8 c10c8 d8d9 b9g9 d7d6 b6e8 i6h6 e5e6 f6e6 c1e4 a6b6 e4b6 d6d5 c4c5 d9d10 e3d3 h6i6 c5c6 d5c5"
 +        result = sf.is_immediate_game_end("janggi", JANGGI, moves.split())
 +        self.assertFalse(result[0])
 +
 +        moves = "e2e3 e9f9 h3d3 e7f7 i1i3 h10i8 i3h3 c10e7 h3h8 i10i9 h8b8 i9g9 d3f3 f9e9 f3f10 e7c10 f10c10 b10c8 c10g10 g9f9 b8c8 a10b10 b3f3 f9h9 a1a2 h9f9 a2d2 b10b9 d2d10 e9d10 c8c10 d10d9 f3f9 i8g9 f9b9 a7a6 g10g7 f7f6 e4e5 c7d7 g1e4 i7i6 e4b6 d9d8 c10c8 d8d9 b9g9 d7d6 b6e8 i6h6 e5e6 f6e6 c1e4 a6b6 e4b6 d6d5 c4c5 d9d10 e3d3 h6i6 c5c6 d5c5 d3d3"
 +        result = sf.is_immediate_game_end("janggi", JANGGI, moves.split())
 +        self.assertTrue(result[0])
 +        self.assertTrue(result[1] < 0)
 +
 +    def test_is_optional_game_end(self):
 +        result = sf.is_optional_game_end("capablanca", CAPA, [])
 +        self.assertFalse(result[0])
 +
 +    def test_has_insufficient_material(self):
 +        for variant, positions in variant_positions.items():
 +            for fen, expected_result in positions.items():
 +                result = sf.has_insufficient_material(variant, fen, [])
 +                self.assertEqual(result, expected_result, "{}: {}".format(variant, fen))
 +
 +    def test_validate_fen(self):
 +        for variant, positions in variant_positions.items():
 +            for fen in positions:
 +                self.assertTrue(sf.validate_fen(fen, variant) == 1, "{}: {}".format(variant, fen))
 +
 +
 +if __name__ == '__main__':
 +    unittest.main(verbosity=2)