stockfish
*.o
.depend
+tests/js/node_modules
+ffish.js
+*.wasm
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("\", "/")
from glob import glob
import platform
import io
+import os
args = ["-DLARGEBOARDS", "-DPRECOMPUTED_MAGICS", "-flto", "-std=c++11"]
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",
--- /dev/null
+/*
+ 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;
+}
--- /dev/null
+/*
+ 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);
+}
#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
return;
}
-}
-
extern "C" PyObject* pyffish_info(PyObject* self) {
return Py_BuildValue("s", engine_info().c_str());
}
--- /dev/null
+# 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
+```
--- /dev/null
+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/')
+});
--- /dev/null
+{
+ "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"
+ }
+}
--- /dev/null
+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);
+ });
+});