From: QueensGambit Date: Mon, 29 Jun 2020 12:22:45 +0000 (+0200) Subject: Added initial version of ffish.js X-Git-Url: http://winboard.nl/cgi-bin?a=commitdiff_plain;h=aef361bb1c1b800bc0fe6b1cff4832386667801c;p=fairystockfish.git Added initial version of ffish.js --- diff --git a/.gitignore b/.gitignore index 417fc40..be1f861 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ stockfish *.o .depend +tests/js/node_modules +ffish.js +*.wasm diff --git a/appveyor.yml b/appveyor.yml index c0928f5..5b75bcc 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -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("\", "/") diff --git a/setup.py b/setup.py index 064b0fb..dc57802 100644 --- 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 index 0000000..1d185ea --- /dev/null +++ b/src/apiutil.h @@ -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 . +*/ + +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(from, to))) + san += std::string("="); + if (is_gating(m)) + san += std::string("/") + pos.piece_to_char()[make_piece(WHITE, gating_type(m))]; + } + + // Check and checkmate + if (pos.gives_check(m) && !is_shogi(n)) + { + StateInfo st; + pos.do_move(m, st); + san += MoveList(pos).size() ? "+" : "#"; + pos.undo_move(m); + } + + return san; +} + +bool hasInsufficientMaterial(Color c, const Position& pos) { + + // 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 index 0000000..41ecdb4 --- /dev/null +++ b/src/ffishjs.cpp @@ -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 . +*/ + +#include +#include +#include +#include + +#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 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(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(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(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(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(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(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") + .constructor<>() + .constructor() + .constructor() + .constructor() + .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); +} diff --git a/src/pyffish.cpp b/src/pyffish.cpp index 814a309..f2d91d5 100644 --- a/src/pyffish.cpp +++ b/src/pyffish.cpp @@ -17,304 +17,10 @@ #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(from, to))) - san += std::string("="); - if (is_gating(m)) - san += std::string("/") + pos.piece_to_char()[make_piece(WHITE, gating_type(m))]; - } - // Check and checkmate - if (pos.gives_check(m) && !is_shogi(n)) - { - StateInfo st; - pos.do_move(m, st); - san += MoveList(pos).size() ? "+" : "#"; - pos.undo_move(m); - } - - return san; -} - -bool hasInsufficientMaterial(Color c, const Position& pos) { - - // 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(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 index 0000000..f216efa --- /dev/null +++ b/tests/js/README.md @@ -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 index 0000000..2ef91ea --- /dev/null +++ b/tests/js/index.js @@ -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 index 0000000..150c016 --- /dev/null +++ b/tests/js/package.json @@ -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 index 0000000..5b29098 --- /dev/null +++ b/tests/js/test.js @@ -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); + }); +});