Added initial version of ffish.js
authorQueensGambit <curry-berry@freenet.de>
Mon, 29 Jun 2020 12:22:45 +0000 (14:22 +0200)
committerFabian Fichter <ianfab@users.noreply.github.com>
Mon, 29 Jun 2020 16:38:06 +0000 (18:38 +0200)
.gitignore
appveyor.yml
setup.py
src/apiutil.h [new file with mode: 0644]
src/ffishjs.cpp [new file with mode: 0644]
src/pyffish.cpp
tests/js/README.md [new file with mode: 0644]
tests/js/index.js [new file with mode: 0644]
tests/js/package.json [new file with mode: 0644]
tests/js/test.js [new file with mode: 0644]

index 417fc40..be1f861 100644 (file)
@@ -1,3 +1,6 @@
 stockfish
 *.o
 .depend
+tests/js/node_modules
+ffish.js
+*.wasm
index c0928f5..5b75bcc 100644 (file)
@@ -32,7 +32,7 @@ init:
 before_build:
   - ps: |
       # Get sources
-      $src = get-childitem -Path *.cpp -Recurse -Exclude pyffish.cpp | select -ExpandProperty FullName
+      $src = get-childitem -Path *.cpp -Recurse -Exclude pyffish.cpp,ffishjs.cpp | select -ExpandProperty FullName
       $src = $src -join ' '
       $src = $src.Replace("\", "/")
 
index 064b0fb..dc57802 100644 (file)
--- a/setup.py
+++ b/setup.py
@@ -4,6 +4,7 @@ from setuptools import setup, Extension
 from glob import glob
 import platform
 import io
+import os
 
 
 args = ["-DLARGEBOARDS", "-DPRECOMPUTED_MAGICS", "-flto", "-std=c++11"]
@@ -24,9 +25,16 @@ CLASSIFIERS = [
 with io.open("Readme.md", "r", encoding="utf8") as fh:
     long_description = fh.read().strip()
 
+sources = glob("src/*.cpp") + glob("src/syzygy/*.cpp")
+ffish_source_file = os.path.normcase("src/ffishjs.cpp")
+try:
+    sources.remove(ffish_source_file)
+except ValueError:
+    print(f"ffish_source_file {ffish_source_file} was not found in sources {sources}.")
+
 pyffish_module = Extension(
     "pyffish",
-    sources=glob("src/*.cpp") + glob("src/syzygy/*.cpp"),
+    sources=sources,
     extra_compile_args=args)
 
 setup(name="pyffish", version="0.0.50",
diff --git a/src/apiutil.h b/src/apiutil.h
new file mode 100644 (file)
index 0000000..1d185ea
--- /dev/null
@@ -0,0 +1,310 @@
+/*
+  Fairy-Stockfish, a UCI chess variant playing engine derived from Stockfish
+  Copyright (C) 2018-2020 Fabian Fichter
+
+  Fairy-Stockfish is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  Fairy-Stockfish is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+namespace PSQT {
+  void init(const Variant* v);
+}
+
+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
+    NOTATION_SHOGI_HODGES_NUMBER, // Examples: P-76, S*34
+    // 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_NUMBER;
+    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 || n == NOTATION_SHOGI_HODGES_NUMBER;
+}
+
+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 (is_shogi(n) && 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 (is_shogi(n) && 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:
+    case NOTATION_SHOGI_HODGES_NUMBER:
+        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:
+    case NOTATION_SHOGI_HODGES_NUMBER:
+        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)
+    {
+        // Disambiguate by rank (+/-) if target square of other piece is valid
+        if (popcount(pos.pieces(us, pt) & file_bb(from)) == 2)
+        {
+            Square otherFrom = lsb((pos.pieces(us, pt) & file_bb(from)) ^ from);
+            Square otherTo = otherFrom + Direction(to) - Direction(from);
+            if (is_ok(otherTo) && (pos.board_bb(us, pt) & otherTo))
+                return RANK_DISAMBIGUATION;
+        }
+        return FILE_DISAMBIGUATION;
+    }
+
+    // Pawn captures always use disambiguation
+    if (n == NOTATION_SAN && pt == PAWN)
+    {
+        if (pos.capture(m))
+            return FILE_DISAMBIGUATION;
+        if (type_of(m) == PROMOTION && from != to && pos.sittuyin_promotion())
+            return SQUARE_DISAMBIGUATION;
+    }
+
+    // A disambiguation occurs if we have more then one piece of type 'pt'
+    // that can reach 'to' with a legal move.
+    Bitboard b = pos.pieces(us, pt) ^ from;
+    Bitboard others = 0;
+
+    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 "";
+    }
+}
+
+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 (is_gating(m))
+        {
+            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_HOSKING ? '\'' : is_shogi(n) ? '*' : '@';
+        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 (pos.capture(m))
+            san += 'x';
+        else if (n == NOTATION_LAN || (is_shogi(n) && (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 += is_shogi(n) ? std::string("+") : std::string("=") + pos.piece_to_char()[make_piece(WHITE, pos.promoted_piece_type(type_of(pos.moved_piece(m))))];
+        else if (type_of(m) == PIECE_DEMOTION)
+            san += is_shogi(n) ? std::string("-") : std::string("=") + std::string(1, pos.piece_to_char()[pos.unpromoted_piece_on(from)]);
+        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) {
+
+    // Other win rules
+    if (   pos.captures_to_hand()
+        || pos.count_in_hand(c, ALL_PIECES)
+        || pos.extinction_value() != VALUE_NONE
+        || (pos.capture_the_flag_piece() && pos.count(c, pos.capture_the_flag_piece())))
+        return false;
+
+    // Restricted pieces
+    Bitboard restricted = pos.pieces(~c, KING);
+    for (PieceType pt : pos.piece_types())
+        if (pt == KING || !(pos.board_bb(c, pt) & pos.board_bb(~c, KING)))
+            restricted |= pos.pieces(c, pt);
+
+    // Mating pieces
+    for (PieceType pt : { ROOK, QUEEN, ARCHBISHOP, CHANCELLOR, SILVER, GOLD, COMMONER, CENTAUR })
+        if ((pos.pieces(c, pt) & ~restricted) || (pos.count(c, PAWN) && pos.promotion_piece_types().find(pt) != pos.promotion_piece_types().end()))
+            return false;
+
+    // Color-bound pieces
+    Bitboard colorbound = 0, unbound;
+    for (PieceType pt : { BISHOP, FERS, FERS_ALFIL, ALFIL, ELEPHANT })
+        colorbound |= pos.pieces(pt) & ~restricted;
+    unbound = pos.pieces() ^ restricted ^ colorbound;
+    if ((colorbound & pos.pieces(c)) && (((DarkSquares & colorbound) && (~DarkSquares & colorbound)) || unbound))
+        return false;
+
+    // Unbound pieces require one helper piece of either color
+    if ((pos.pieces(c) & unbound) && (popcount(pos.pieces() ^ restricted) >= 2 || pos.stalemate_value() != VALUE_DRAW))
+        return false;
+
+    return true;
+}
diff --git a/src/ffishjs.cpp b/src/ffishjs.cpp
new file mode 100644 (file)
index 0000000..41ecdb4
--- /dev/null
@@ -0,0 +1,235 @@
+/*
+  ffish.js, a JavaScript chess variant library derived from Fairy-Stockfish
+  Copyright (C) 2020 Fabian Fichter, Johannes Czech
+
+  ffish.js is free software: you can redistribute it and/or modify
+  it under the terms of the GNU General Public License as published by
+  the Free Software Foundation, either version 3 of the License, or
+  (at your option) any later version.
+
+  ffish.js is distributed in the hope that it will be useful,
+  but WITHOUT ANY WARRANTY; without even the implied warranty of
+  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+  GNU General Public License for more details.
+
+  You should have received a copy of the GNU General Public License
+  along with this program.  If not, see <http://www.gnu.org/licenses/>.
+*/
+
+#include <emscripten.h>
+#include <emscripten/bind.h>
+#include <vector>
+#include <string>
+
+#include "misc.h"
+#include "types.h"
+#include "bitboard.h"
+#include "evaluate.h"
+#include "position.h"
+#include "search.h"
+#include "syzygy/tbprobe.h"
+#include "thread.h"
+#include "tt.h"
+#include "uci.h"
+#include "piece.h"
+#include "variant.h"
+#include "movegen.h"
+#include "apiutil.h"
+
+using namespace emscripten;
+
+
+void initializeStockfish(std::string& uciVariant) {
+  pieceMap.init();
+  variants.init();
+  UCI::init(Options);
+  PSQT::init(variants.find(uciVariant)->second);
+  Bitboards::init();
+  Position::init();
+  Bitbases::init();
+}
+
+class Board {
+  // note: we can't use references for strings here due to conversion to JavaScript
+private:
+  const Variant* v;
+  StateListPtr states;
+  Position pos;
+  Thread* thread;
+  std::vector<Move> moveStack;
+ static bool sfInitialized;
+  bool is960;
+
+public:
+  Board():
+    Board("chess", "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1" , false) {
+  }
+
+  Board(std::string uciVariant) {
+    init(uciVariant, "", is960);
+  }
+
+  Board(std::string uciVariant, std::string fen):
+    Board(uciVariant, fen , false) {
+  }
+
+  Board(std::string uciVariant, std::string fen, bool is960) {
+    init(uciVariant, fen, is960);
+  }
+
+  std::string legal_moves() {
+    std::string moves = "";
+    bool first = true;
+    for (const ExtMove& move : MoveList<LEGAL>(this->pos)) {
+        if (first) {
+          moves = UCI::move(this->pos, move);
+          first = false;
+        }
+        else
+          moves += " "  + UCI::move(this->pos, move);
+    }
+    return moves;
+  }
+
+  std::string legal_moves_san() {
+    std::string movesSan = "";
+    bool first = true;
+    for (const ExtMove& move : MoveList<LEGAL>(this->pos)) {
+        if (first) {
+          movesSan = move_to_san(this->pos, move, NOTATION_SAN);
+          first = false;
+        }
+        else
+          movesSan += " "  + move_to_san(this->pos, move, NOTATION_SAN);
+    }
+    return movesSan;
+  }
+
+  int number_legal_moves() {
+    return MoveList<LEGAL>(pos).size();
+  }
+
+  void push(std::string uciMove) {
+    do_move(UCI::to_move(this->pos, uciMove));
+  }
+
+  // TODO: This is a naive implementation which compares all legal SAN moves with the requested string.
+  // If the SAN move wasn't found the position remains unchanged. Alternatively, implement a direct conversion.
+   void push_san(std::string sanMove) {
+     Move foundMove = MOVE_NONE;
+     for (const ExtMove& move : MoveList<LEGAL>(pos)) {
+        if (sanMove == move_to_san(this->pos, move, NOTATION_SAN)) {
+          foundMove = move;
+          break;
+        }
+     }
+     if (foundMove != MOVE_NONE)
+        do_move(foundMove);
+   }
+
+
+  void pop() {
+    pos.undo_move(this->moveStack.back());
+    moveStack.pop_back();
+    states->pop_back();
+  }
+
+   void reset() {
+     set_fen(v->startFen);
+   }
+
+   bool is_960() {
+     return is960;
+   }
+
+   std::string fen() const {
+     return this->pos.fen();
+   }
+
+  void set_fen(std::string fen) {
+    resetStates();
+    moveStack.clear();
+    pos.set(v, fen, is960, &states->back(), thread);
+  }
+
+  // note: const identifier for pos not possible due to move_to_san()
+  std::string san_move(std::string uciMove) {
+    return move_to_san(this->pos, UCI::to_move(this->pos, uciMove), NOTATION_SAN);
+  }
+
+  // returns true for WHITE and false for BLACK
+  bool turn() {
+    return !pos.side_to_move();
+  }
+
+  int halfmove_clock() {
+    return pos.rule50_count();
+  }
+
+  int game_ply() {
+    return pos.game_ply();
+  }
+
+  bool is_game_over() {
+    for (const ExtMove& move : MoveList<LEGAL>(pos)) {
+        return false;
+    }
+    return true;
+  }
+
+  // TODO: return board in ascii notation
+  // static std::string get_string_from_instance(const Board& board) {
+  // }
+
+private:
+  void resetStates() {
+    this->states = StateListPtr(new std::deque<StateInfo>(1));
+  }
+
+  void do_move(Move move) {
+    states->emplace_back();
+    this->pos.do_move(move, states->back());
+    this->moveStack.emplace_back(move);
+  }
+
+  void init(std::string& uciVariant, std::string fen, bool is960) {
+   if (!Board::sfInitialized) {
+        initializeStockfish(uciVariant);
+       Board::sfInitialized = true;
+   }
+    this->v = variants.find(uciVariant)->second;
+    this->resetStates();
+    if (fen == "")
+      fen = v->startFen;
+    this->pos.set(this->v, fen, is960, &this->states->back(), this->thread);
+    this->is960 = is960;
+  }
+};
+
+bool Board::sfInitialized = false;
+
+// binding code
+EMSCRIPTEN_BINDINGS(ffish_js) {
+  class_<Board>("Board")
+    .constructor<>()
+    .constructor<std::string>()
+    .constructor<std::string, std::string>()
+    .constructor<std::string, std::string, bool>()
+    .function("legalMoves", &Board::legal_moves)
+    .function("legalMovesSan", &Board::legal_moves_san)
+    .function("numberLegalMoves", &Board::number_legal_moves)
+    .function("push", &Board::push)
+    .function("pushSan", &Board::push_san)
+    .function("pop", &Board::pop)
+    .function("reset", &Board::reset)
+    .function("is960", &Board::is_960)
+    .function("fen", &Board::fen)
+    .function("setFen", &Board::set_fen)
+    .function("sanMove", &Board::san_move)
+    .function("turn", &Board::turn)
+    .function("halfmoveClock", &Board::halfmove_clock)
+    .function("gamePly", &Board::game_ply)
+    .function("isGameOver", &Board::is_game_over);
+    // TODO: enable to string conversion method
+    // .class_function("getStringFromInstance", &Board::get_string_from_instance);
+}
index 814a309..f2d91d5 100644 (file)
 #include "uci.h"
 #include "piece.h"
 #include "variant.h"
+#include "apiutil.h"
 
-static PyObject* PyFFishError;
-
-namespace PSQT {
-  void init(const Variant* v);
-}
-
-namespace
-{
-
-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
-    NOTATION_SHOGI_HODGES_NUMBER, // Examples: P-76, S*34
-    // 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_NUMBER;
-    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 || n == NOTATION_SHOGI_HODGES_NUMBER;
-}
-
-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 (is_shogi(n) && 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 (is_shogi(n) && 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:
-    case NOTATION_SHOGI_HODGES_NUMBER:
-        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:
-    case NOTATION_SHOGI_HODGES_NUMBER:
-        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)
-    {
-        // Disambiguate by rank (+/-) if target square of other piece is valid
-        if (popcount(pos.pieces(us, pt) & file_bb(from)) == 2)
-        {
-            Square otherFrom = lsb((pos.pieces(us, pt) & file_bb(from)) ^ from);
-            Square otherTo = otherFrom + Direction(to) - Direction(from);
-            if (is_ok(otherTo) && (pos.board_bb(us, pt) & otherTo))
-                return RANK_DISAMBIGUATION;
-        }
-        return FILE_DISAMBIGUATION;
-    }
-
-    // Pawn captures always use disambiguation
-    if (n == NOTATION_SAN && pt == PAWN)
-    {
-        if (pos.capture(m))
-            return FILE_DISAMBIGUATION;
-        if (type_of(m) == PROMOTION && from != to && pos.sittuyin_promotion())
-            return SQUARE_DISAMBIGUATION;
-    }
-
-    // A disambiguation occurs if we have more then one piece of type 'pt'
-    // that can reach 'to' with a legal move.
-    Bitboard b = pos.pieces(us, pt) ^ from;
-    Bitboard others = 0;
-
-    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 "";
-    }
-}
-
-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 (is_gating(m))
-        {
-            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_HOSKING ? '\'' : is_shogi(n) ? '*' : '@';
-        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 (pos.capture(m))
-            san += 'x';
-        else if (n == NOTATION_LAN || (is_shogi(n) && (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 += is_shogi(n) ? std::string("+") : std::string("=") + pos.piece_to_char()[make_piece(WHITE, pos.promoted_piece_type(type_of(pos.moved_piece(m))))];
-        else if (type_of(m) == PIECE_DEMOTION)
-            san += is_shogi(n) ? std::string("-") : std::string("=") + std::string(1, pos.piece_to_char()[pos.unpromoted_piece_on(from)]);
-        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) {
-
-    // Other win rules
-    if (   pos.captures_to_hand()
-        || pos.count_in_hand(c, ALL_PIECES)
-        || pos.extinction_value() != VALUE_NONE
-        || (pos.capture_the_flag_piece() && pos.count(c, pos.capture_the_flag_piece())))
-        return false;
-
-    // Restricted pieces
-    Bitboard restricted = pos.pieces(~c, KING);
-    for (PieceType pt : pos.piece_types())
-        if (pt == KING || !(pos.board_bb(c, pt) & pos.board_bb(~c, KING)))
-            restricted |= pos.pieces(c, pt);
-
-    // Mating pieces
-    for (PieceType pt : { ROOK, QUEEN, ARCHBISHOP, CHANCELLOR, SILVER, GOLD, COMMONER, CENTAUR })
-        if ((pos.pieces(c, pt) & ~restricted) || (pos.count(c, PAWN) && pos.promotion_piece_types().find(pt) != pos.promotion_piece_types().end()))
-            return false;
-
-    // Color-bound pieces
-    Bitboard colorbound = 0, unbound;
-    for (PieceType pt : { BISHOP, FERS, FERS_ALFIL, ALFIL, ELEPHANT })
-        colorbound |= pos.pieces(pt) & ~restricted;
-    unbound = pos.pieces() ^ restricted ^ colorbound;
-    if ((colorbound & pos.pieces(c)) && (((DarkSquares & colorbound) && (~DarkSquares & colorbound)) || unbound))
-        return false;
-
-    // Unbound pieces require one helper piece of either color
-    if ((pos.pieces(c) & unbound) && (popcount(pos.pieces() ^ restricted) >= 2 || pos.stalemate_value() != VALUE_DRAW))
-        return false;
-
-    return true;
-}
+static PyObject* PyFFishError;
 
 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
@@ -343,8 +49,6 @@ void buildPosition(Position& pos, StateListPtr& states, const char *variant, con
     return;
 }
 
-}
-
 extern "C" PyObject* pyffish_info(PyObject* self) {
     return Py_BuildValue("s", engine_info().c_str());
 }
diff --git a/tests/js/README.md b/tests/js/README.md
new file mode 100644 (file)
index 0000000..f216efa
--- /dev/null
@@ -0,0 +1,98 @@
+# ffish.js
+
+**ffish.js** is a high performance JavaScript library which supports all chess variants of _FairyStockfish_.
+
+It is built using emscripten/Embind from C++ source code.
+
+* https://emscripten.org/docs/porting/connecting_cpp_and_javascript/embind.html
+
+## Build instuctions
+
+```bash
+cd ../../src
+```
+```bash
+emcc -O3 --bind ffishjs.cpp \
+benchmark.cpp \
+bitbase.cpp \
+bitboard.cpp \
+endgame.cpp \
+evaluate.cpp \
+material.cpp \
+misc.cpp \
+movegen.cpp \
+movepick.cpp \
+parser.cpp \
+partner.cpp \
+pawns.cpp \
+piece.cpp \
+position.cpp \
+psqt.cpp \
+search.cpp \
+thread.cpp \
+timeman.cpp \
+tt.cpp \
+uci.cpp \
+syzygy/tbprobe.cpp \
+ucioption.cpp \
+variant.cpp \
+xboard.cpp \
+-o ../tests/js/ffish.js
+```
+
+## Examples
+
+Load the API in JavaScript:
+
+```javascript
+const ffish = require('./ffish.js');
+```
+
+Create a new variant board from its default starting position:
+
+```javascript
+// create new board with starting position
+let board = new ffish.Board("chess");
+```
+
+Set a custom fen position:
+```javascript
+board.setFen("rnb1kbnr/ppp1pppp/8/3q4/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 3");
+```
+
+Initialize a board with a custom FEN:
+```javascript
+board = new ffish.Board("crazyhouse", "rnb1kb1r/ppp2ppp/4pn2/8/3P4/2N2Q2/PPP2PPP/R1B1KB1R/QPnp b KQkq - 0 6");
+// create a new board object for a given fen
+let board2 = new ffish.Board("crazyhouse", );
+```
+
+Add a new move:
+```javascript
+board.push("g2g4");
+```
+
+Generate all legal moves in UCI and SAN notation:
+```javascript
+let legalMoves = board.legalMoves().split(" ");
+let legalMovesSan = board.legalMovesSan().split(" ");
+
+for (var i = 0; i < legalMovesSan.length; i++) {
+    console.log(`${i}: ${legalMoves[i]}, ${legalMoves
+```
+
+For examples for every function see [test.js](./test.js).
+
+## Instructions to run the tests
+```bash
+npm install
+npm test
+```
+
+## Instructions to run the example server
+```bash
+npm install
+```
+```bash
+node index.js
+```
diff --git a/tests/js/index.js b/tests/js/index.js
new file mode 100644 (file)
index 0000000..2ef91ea
--- /dev/null
@@ -0,0 +1,91 @@
+const express = require('express')
+const ffish = require('./ffish.js');
+const { PerformanceObserver, performance } = require('perf_hooks');
+const { Chess } = require('chess.js')
+const { Crazyhouse } = require('crazyhouse.js')
+
+const app = express();
+
+app.get('/', (req, res) => {
+
+let board = new ffish.Board("chess"); //, "rnb1kbnr/ppp1pppp/8/3q4/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 3", false);
+let legalMoves = board.legalMoves();
+
+let it = 1000;
+
+console.log("Standard Chess")
+console.log("==================")
+
+var t0 = performance.now()
+for (let i = 0; i < it; ++i) {
+   legalMoves = board.legalMoves().split(" ");
+}
+var t1 = performance.now()
+console.log(`Call to board.legalMoves()+legalMoves.split(" ") took ${(t1 - t0).toFixed(2)}  milliseconds.`)
+
+var t0 = performance.now()
+for (let i = 0; i < it; ++i) {
+  legalMoves = board.legalMovesSan().split(" ")
+}
+var t1 = performance.now()
+console.log(`board.legalMovesSan().split(" ").length: ${legalMoves.length}`)
+console.log(`Call to board.legalMovesSan()+legalMoves.split(" ") took ${(t1 - t0).toFixed(2)}  milliseconds.`)
+
+
+// pass in a FEN string to load a particular position
+const chess = new Chess(
+    "rnb1kbnr/ppp1pppp/8/3q4/8/8/PPPP1PPP/RNBQKBNR w KQkq - 0 3"
+)
+var t0 = performance.now()
+for (let i = 0; i < it; ++i) {
+  legalMoves = chess.moves()
+}
+var t1 = performance.now()
+console.log(`chess.moves().length: ${legalMoves.length}`)
+console.log(`Call to chess.moves() took ${(t1 - t0).toFixed(2)}  milliseconds.`)
+
+console.log("Crazyhouse")
+console.log("===========")
+
+let crazyhouseFen = "rnb1kb1r/ppp2ppp/4pn2/8/3P4/2N2Q2/PPP2PPP/R1B1KB1R/QPnp b KQkq - 0 6";
+board = new ffish.Board("crazyhouse", crazyhouseFen);
+
+var t0 = performance.now()
+for (let i = 0; i < it; ++i) {
+  legalMoves = board.legalMovesSan().split(" ")
+}
+var t1 = performance.now()
+console.log(`board.legalMoves().split(" ").length: ${legalMoves.length}`)
+console.log(`Call to board.legalMoves() took ${(t1 - t0).toFixed(2)}  milliseconds.`)
+
+cz_moves = ["e4", "d5", "exd5", "Qxd5", "Nf3", "Nf6", "Nc3", "e6", "d4", "Qxf3", "Qxf3"]
+// pass in a FEN string to load a particular position
+const crazyhouse = new Crazyhouse()
+
+for (let idx = 0; idx < cz_moves.length; ++idx) {
+  crazyhouse.move(cz_moves[idx])
+}
+
+var t0 = performance.now()
+for (let i = 0; i < it; ++i) {
+  legalMoves = crazyhouse.moves()
+}
+var t1 = performance.now()
+console.log(`crazyhouse.moves().length: ${legalMoves.length}`)
+console.log(`Call to crazyhouse.moves() took ${(t1 - t0).toFixed(2)}  milliseconds.`)
+
+
+let legalMovesSan = board.legalMovesSan().split(" ");
+
+for (var i = 0; i < legalMovesSan.length; i++) {
+    console.log(`${i}: ${legalMoves[i]}, ${legalMovesSan[i]}`);
+}
+console.log(board.fen());
+
+  res.send(String("Test server of ffish.js"));
+});
+
+app.listen(8000, () => {
+  console.log('Test server of ffish.js listening on port 8000.')
+  console.log('http://127.0.0.1:8000/')
+});
diff --git a/tests/js/package.json b/tests/js/package.json
new file mode 100644 (file)
index 0000000..150c016
--- /dev/null
@@ -0,0 +1,22 @@
+{
+  "name": "ffish_test",
+  "version": "1.0.0",
+  "description": "Testing server for ffish.js",
+  "main": "index.js",
+  "scripts": {
+    "test": "mocha"
+  },
+  "author": "Fabian Fichter, Johannes Czech",
+  "license": "GPL-3.0",
+  "dependencies": {
+    "chess": "^0.4.3",
+    "chess.js": "^0.11.0",
+    "crazyhouse.js": "0.0.8",
+    "express": "^4.17.1",
+    "performance": "^1.4.0"
+  },
+  "devDependencies": {
+    "chai": "^4.2.0",
+    "mocha": "^8.0.1"
+  }
+}
diff --git a/tests/js/test.js b/tests/js/test.js
new file mode 100644 (file)
index 0000000..5b29098
--- /dev/null
@@ -0,0 +1,200 @@
+before(() => {
+  chai = require('chai');
+  return new Promise((resolve) => {
+    ffish = require('./ffish.js');
+    ffish['onRuntimeInitialized'] = () => {
+      resolve();
+    }
+  });
+});
+
+describe('Constructor: no parameter ', function () {
+  it("it creates a chess board from the default position", () => {
+    const board = new ffish.Board();
+    chai.expect(board.fen()).to.equal("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
+    chai.expect(board.is960()).to.equal(false);
+  });
+});
+
+describe('Constructor: variant parameter ', function () {
+  it("it creates a board object from a given UCI-variant", () => {
+    const board = new ffish.Board("chess");
+    chai.expect(board.fen()).to.equal("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
+    chai.expect(board.is960()).to.equal(false);
+  });
+});
+
+describe('Constructor: variant parameter + fen ', function () {
+  it("it creates a board object for a given UCI-variant with a given FEN", () => {
+    const board = new ffish.Board("crazyhouse", "rnbqkb1r/pp3ppp/5p2/2pp4/8/5N2/PPPP1PPP/RNBQKB1R/Np w KQkq - 0 5");
+    chai.expect(board.fen()).to.equal("rnbqkb1r/pp3ppp/5p2/2pp4/8/5N2/PPPP1PPP/RNBQKB1R[Np] w KQkq - 0 5");
+    chai.expect(board.is960()).to.equal(false);
+  });
+});
+
+describe('Constructor: variant parameter + fen + is960', function () {
+  it("it creates a board object for a given UCI-variant with a given FEN and is960 identifier", () => {
+    const board = new ffish.Board("chess", "rnknb1rq/pp2ppbp/3p2p1/2p5/4PP2/2N1N1P1/PPPP3P/R1K1BBRQ b KQkq - 1 5", true);
+    chai.expect(board.fen()).to.equal("rnknb1rq/pp2ppbp/3p2p1/2p5/4PP2/2N1N1P1/PPPP3P/R1K1BBRQ b GAga - 1 5");
+    chai.expect(board.is960()).to.equal(true);
+  });
+});
+
+describe('board.legalMoves()', function () {
+  it("it returns all legal moves in uci notation as a concatenated string", () => {
+    const board = new ffish.Board("crazyhouse", "r1b3nr/pppp1kpp/2n5/2b1p3/4P3/2N5/PPPP1PPP/R1B1K1NR/QPbq w KQ - 0 7");
+    const expectedMoves = 'a2a3 b2b3 d2d3 f2f3 g2g3 h2h3 a2a4 b2b4 d2d4 f2f4 g2g4 h2h4 c3b1 c3d1 c3e2 c3a4 c3b5 c3d5' +
+        ' g1e2 g1f3 g1h3 a1b1 P@e2 P@a3 P@b3 P@d3 P@e3 P@f3 P@g3 P@h3 P@a4 P@b4 P@c4 P@d4 P@f4 P@g4 P@h4 P@a5 P@b5' +
+        ' P@d5 P@f5 P@g5 P@h5 P@a6 P@b6 P@d6 P@e6 P@f6 P@g6 P@h6 P@e7 Q@b1 Q@d1 Q@f1 Q@e2 Q@a3 Q@b3 Q@d3 Q@e3 Q@f3 ' +
+        'Q@g3 Q@h3 Q@a4 Q@b4 Q@c4 Q@d4 Q@f4 Q@g4 Q@h4 Q@a5 Q@b5 Q@d5 Q@f5 Q@g5 Q@h5 Q@a6 Q@b6 Q@d6 Q@e6 Q@f6 Q@g6' +
+        ' Q@h6 Q@e7 Q@b8 Q@d8 Q@e8 Q@f8 e1d1 e1f1 e1e2';
+    chai.expect(board.legalMoves()).to.equal(expectedMoves);
+  });
+});
+
+describe('board.legalMovesSan()', function () {
+  it("it returns all legal moves in SAN notation as a concatenated string", () => {
+    const board = new ffish.Board("crazyhouse", "r1b3nr/pppp1kpp/2n5/2b1p3/4P3/2N5/PPPP1PPP/R1B1K1NR/QPbq w KQ - 0 7");
+    const expectedMoves = 'a3 b3 d3 f3 g3 h3 a4 b4 d4 f4 g4 h4 Nb1 Nd1 Nce2 Na4 Nb5 Nd5 Nge2 Nf3 Nh3 Rb1 P@e2 P@a3' +
+        ' P@b3 P@d3 P@e3 P@f3 P@g3 P@h3 P@a4 P@b4 P@c4 P@d4 P@f4 P@g4 P@h4 P@a5 P@b5 P@d5 P@f5 P@g5 P@h5 P@a6 P@b6' +
+        ' P@d6 P@e6+ P@f6 P@g6+ P@h6 P@e7 Q@b1 Q@d1 Q@f1 Q@e2 Q@a3 Q@b3+ Q@d3 Q@e3 Q@f3+ Q@g3 Q@h3 Q@a4 Q@b4 Q@c4+' +
+        ' Q@d4 Q@f4+ Q@g4 Q@h4 Q@a5 Q@b5 Q@d5+ Q@f5+ Q@g5 Q@h5+ Q@a6 Q@b6 Q@d6 Q@e6+ Q@f6+ Q@g6+ Q@h6 Q@e7+ Q@b8' +
+        ' Q@d8 Q@e8+ Q@f8+ Kd1 Kf1 Ke2';
+    chai.expect(board.legalMovesSan()).to.equal(expectedMoves);
+  });
+});
+
+describe('board.numberLegalMoves()', function () {
+  it("it returns all legal moves in uci notation as a concatenated string", () => {
+    const board = new ffish.Board("crazyhouse", "r1b3nr/pppp1kpp/2n5/2b1p3/4P3/2N5/PPPP1PPP/R1B1K1NR/QPbq w KQ - 0 7");
+    chai.expect(board.numberLegalMoves()).to.equal(90);
+  });
+});
+
+describe('board.push()', function () {
+  it("it pushes a move in uci notation to the board", () => {
+    let board = new ffish.Board();
+    board.push("e2e4");
+    board.push("e7e5");
+    board.push("g1f3");
+    chai.expect(board.fen()).to.equal("rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2");
+  });
+});
+
+describe('board.pushSan()', function () {
+  it("it pushes a move in san notation to the board", () => {
+    let board = new ffish.Board();
+    board.pushSan("e4");
+    board.pushSan("e5");
+    board.pushSan("Nf3");
+    chai.expect(board.fen()).to.equal("rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2");
+  });
+});
+
+describe('board.pop()', function () {
+  it("it undos the last move", () => {
+    let board = new ffish.Board();
+    board.push("e2e4");
+    board.push("e7e5");
+    board.pop();
+    board.push("e7e5");
+    board.push("g1f3");
+    board.push("b8c6");
+    board.push("f1b5");
+    board.pop();
+    board.pop();
+
+    chai.expect(board.fen()).to.equal("rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2");
+  });
+});
+
+describe('board.reset()', function () {
+  it("it resets the board to its starting position", () => {
+    let board = new ffish.Board();
+    board.pushSan("e4");
+    board.pushSan("e5");
+    board.pushSan("Nf3");
+    board.reset();
+    chai.expect(board.fen()).to.equal("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
+  });
+});
+
+describe('board.is960()', function () {
+  it("it checks if the board originates from a 960 position", () => {
+    let board = new ffish.Board();
+    chai.expect(board.is960()).to.equal(false);
+    const board2 = new ffish.Board("chess", "rnknb1rq/pp2ppbp/3p2p1/2p5/4PP2/2N1N1P1/PPPP3P/R1K1BBRQ b KQkq - 1 5", true);
+    chai.expect(board2.is960()).to.equal(true);
+  });
+});
+
+describe('board.fen()', function () {
+  it("it returns the current position in fen format", () => {
+    let board = new ffish.Board();
+    chai.expect(board.fen()).to.equal("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
+  });
+});
+
+describe('board.setFen()', function () {
+  it("it sets a custom position via fen", () => {
+    let board = new ffish.Board();
+    board.setFen("r1bqkbnr/ppp2ppp/2np4/1B6/3NP3/8/PPP2PPP/RNBQK2R b KQkq - 0 5");
+    chai.expect(board.fen()).to.equal("r1bqkbnr/ppp2ppp/2np4/1B6/3NP3/8/PPP2PPP/RNBQK2R b KQkq - 0 5");
+  });
+});
+
+describe('board.sanMove()', function () {
+  it("it converts an uci move into san", () => {
+    const board = new ffish.Board();
+    const san = board.sanMove("g1f3");
+    chai.expect(san).to.equal("Nf3");
+  });
+});
+
+describe('board.turn()', function () {
+  it("it returns the side to move", () => {
+    let board = new ffish.Board();
+    chai.expect(board.turn()).to.equal(true);
+    board.push("e2e4");
+    chai.expect(board.turn()).to.equal(false);
+  });
+});
+
+describe('board.halfmoveClock()', function () {
+  it("it returns the halfmoveClock / 50-move-rule-counter", () => {
+    let board = new ffish.Board();
+    chai.expect(board.halfmoveClock()).to.equal(0);
+    board.push("e2e4");
+    board.push("e7e5");
+    chai.expect(board.halfmoveClock()).to.equal(0);
+    board.push("g1f3");
+    board.push("g8f6");
+    chai.expect(board.halfmoveClock()).to.equal(2);
+    board.push("f3e5");
+    chai.expect(board.halfmoveClock()).to.equal(0);
+  });
+});
+
+describe('board.gamePly()', function () {
+  it("it returns the current game ply", () => {
+    let board = new ffish.Board();
+    chai.expect(board.gamePly()).to.equal(0);
+    board.push("e2e4");
+    chai.expect(board.gamePly()).to.equal(1);
+    board.push("e7e5");
+    board.push("g1f3");
+    board.push("g8f6");
+    board.push("f3e5");
+    chai.expect(board.gamePly()).to.equal(5);
+  });
+});
+
+describe('board.isGameOver()', function () {
+  it("it checks if the game is over based on the number of legal moves", () => {
+    let board = new ffish.Board();
+    chai.expect(board.isGameOver()).to.equal(false);
+    board.setFen("r1bqkb1r/pppp1ppp/2n2n2/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR w KQkq - 4 4");
+    board.pushSan("Qxf7#");
+    chai.expect(board.isGameOver()).to.equal(true);
+  });
+});