Updated ffish.js to 0.4.2 (Closes #185)
authorQueensGambit <curry-berry@freenet.de>
Wed, 30 Sep 2020 08:40:06 +0000 (10:40 +0200)
committerFabian Fichter <ianfab@users.noreply.github.com>
Wed, 30 Sep 2020 15:22:15 +0000 (17:22 +0200)
+ board.moveStack()
+ board.pushSan(sanMove, notation)
+ board.pushMoves(uciMoves)
+ board.pushSanMoves(sanMoves)
+ board.pushSanMoves(sanMoves, notation)
+ ffish.readGamePGN(pgn)
+ game.headerKeys()
+ game.headers(key)
+ game.mainlineMoves()
+ ffish.variants()
+ ffish.loadVariantConfig()

Added parse_istream()
+ to allow reading a variant configuration file via a stringstream

Added NO_THREADS #define
+ to disable threads usage for build

Added custom variants info to README.md

Changed ordering for ffish.loadVariantsConfig() in test.js

src/ffishjs.cpp
src/position.cpp
src/variant.cpp
src/variant.h
tests/js/README.md
tests/js/package.json
tests/js/test.js
tests/pgn/c60_ruy_lopez.pgn [new file with mode: 0644]
tests/pgn/deep_blue_kasparov_1997.pgn [new file with mode: 0644]
tests/pgn/lichess_pgn_2018.12.21_JannLee_vs_CrazyAra.j9eQS4TF.pgn [new file with mode: 0644]

index 462c65a..5f8fe4e 100644 (file)
@@ -21,6 +21,7 @@
 #include <vector>
 #include <string>
 #include <sstream>
+#include<iostream>
 
 #include "misc.h"
 #include "types.h"
 using namespace emscripten;
 
 
-void initialize_stockfish(std::string& uciVariant) {
+void initialize_stockfish() {
   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:
@@ -115,18 +116,25 @@ public:
     do_move(UCI::to_move(this->pos, uciMove));
   }
 
+  bool push_san(std::string sanMove) {
+    return push_san(sanMove, NOTATION_SAN);
+  }
+
   // 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) {
+  bool push_san(std::string sanMove, Notation notation) {
     Move foundMove = MOVE_NONE;
     for (const ExtMove& move : MoveList<LEGAL>(pos)) {
-      if (sanMove == move_to_san(this->pos, move, NOTATION_SAN)) {
+      if (sanMove == move_to_san(this->pos, move, notation)) {
         foundMove = move;
         break;
       }
     }
-    if (foundMove != MOVE_NONE)
+    if (foundMove != MOVE_NONE) {
       do_move(foundMove);
+      return true;
+    }
+    return false;
   }
 
   void pop() {
@@ -159,7 +167,7 @@ public:
   }
 
   std::string san_move(std::string uciMove, Notation notation) {
-    return move_to_san(this->pos, UCI::to_move(this->pos, uciMove), Notation(notation));
+    return move_to_san(this->pos, UCI::to_move(this->pos, uciMove), notation);
   }
   std::string variation_san(std::string uciMoves) {
     return variation_san(uciMoves, NOTATION_SAN, true);
@@ -238,6 +246,37 @@ public:
     return pos.bikjang();
   }
 
+  std::string move_stack() const {
+    if (moveStack.size() == 0) {
+        return "";
+    }
+    std::string moves = UCI::move(pos, moveStack[0]);
+    for(auto it = std::begin(moveStack)+1; it != std::end(moveStack); ++it) {
+      moves += " " + UCI::move(pos, *it);
+    }
+    return moves;
+  }
+
+  void push_moves(std::string uciMoves) {
+      std::stringstream ss(uciMoves);
+      std::string uciMove;
+      while (std::getline(ss, uciMove, ' ')) {
+          push(uciMove);
+      }
+  }
+
+  void push_san_moves(std::string sanMoves) {
+      return push_san_moves(sanMoves, NOTATION_SAN);
+  }
+
+  void push_san_moves(std::string sanMoves, Notation notation) {
+      std::stringstream ss(sanMoves);
+      std::string sanMove;
+      while (std::getline(ss, sanMove, ' ')) {
+          push_san(sanMove, notation);
+      }
+  }
+
   // TODO: return board in ascii notation
   // static std::string get_string_from_instance(const Board& board) {
   // }
@@ -253,33 +292,203 @@ private:
     this->moveStack.emplace_back(move);
   }
 
-  void init(std::string& uciVariant, std::string fen, bool is960) {
+  void init(std::string uciVariant, std::string fen, bool is960) {
     if (!Board::sfInitialized) {
-      initialize_stockfish(uciVariant);
+      initialize_stockfish();
       Board::sfInitialized = true;
     }
+    if (uciVariant == "")
+      uciVariant = "chess";
     this->v = variants.find(uciVariant)->second;
     this->resetStates();
     if (fen == "")
-    fen = v->startFen;
+      fen = v->startFen;
     this->pos.set(this->v, fen, is960, &this->states->back(), this->thread);
     this->is960 = is960;
   }
 };
 
+bool Board::sfInitialized = false;
+
+namespace ffish {
+
 // returns the version of the Fairy-Stockfish binary
 std::string info() {
   return engine_info();
 }
 
-bool Board::sfInitialized = false;
-
 template <typename T>
 void set_option(std::string name, T value) {
   Options[name] = value;
   Board::sfInitialized = false;
 }
 
+std::string available_variants() {
+    bool first = true;
+    std::string availableVariants = "";
+    for (std::string variant : variants.get_keys()) {
+        if (first) {
+            first = false;
+            availableVariants = variant;
+        }
+        else
+            availableVariants += " " + variant;
+    }
+    return availableVariants;
+}
+
+void load_variant_config(std::string variantInitContent) {
+    std::stringstream ss(variantInitContent);
+    if (!Board::sfInitialized)
+        initialize_stockfish();
+    variants.parse_istream<false>(ss);
+    Options["UCI_Variant"].set_combo(variants.get_keys());
+    Board::sfInitialized = true;
+}
+}
+
+class Game {
+    private:
+        std::unordered_map<std::string, std::string> header;
+        std::unique_ptr<Board> board;
+        std::string variant = "chess";
+        std::string fen = ""; // start pos
+        bool is960 = false;
+        bool parsedGame = false;
+    public:
+        std::string header_keys() {
+            std::string keys = "";
+            bool first = true;
+            for (auto it = header.begin(); it != header.end(); ++it) {
+                if (first) {
+                    keys = it->first;
+                    first = false;
+                }
+                else
+                    keys += " " + it->first;
+            }
+            return keys;
+        }
+
+        std::string headers(std::string item) {
+            auto it = header.find(item);
+            if (it == header.end())
+                return "";
+            return it->second;
+        }
+
+        std::string mainline_moves() {
+            if (!parsedGame)
+                return "";
+            return board->move_stack();
+        }
+
+    friend Game read_game_pgn(std::string);
+};
+
+
+Game read_game_pgn(std::string pgn) {
+    Game game;
+    size_t lineStart = 0;
+    bool headersParsed = false;
+
+    while(true) {
+        size_t lineEnd = pgn.find('\n', lineStart);
+
+        if (lineEnd == std::string::npos)
+            lineEnd = pgn.size();
+
+        if (!headersParsed && pgn[lineStart] == '[') {
+            // parse header
+            // look for item
+            size_t headerKeyStart = lineStart+1;
+            size_t headerKeyEnd = pgn.find(' ', lineStart);
+            size_t headerItemStart = headerKeyEnd+2;
+            size_t headerItemEnd = pgn.find(']', headerKeyEnd)-1;
+
+            // put item into list
+            game.header[pgn.substr(headerKeyStart, headerKeyEnd-headerKeyStart)] = pgn.substr(headerItemStart, headerItemEnd-headerItemStart);
+        }
+        else {
+            if (!headersParsed) {
+                headersParsed = true;
+                auto it = game.header.find("Variant");
+                if (it != game.header.end()) {
+                    game.variant = it->second;
+                    std::transform(game.variant.begin(), game.variant.end(), game.variant.begin(),
+                              [](unsigned char c){ return std::tolower(c); });
+                    game.is960 = it->second.find("960") != std::string::npos;
+                }
+
+                it = game.header.find("FEN");
+                if (it != game.header.end())
+                    game.fen = it->second;
+
+                game.board = std::make_unique<Board>(game.variant, game.fen, game.is960);
+                game.parsedGame = true;
+            }
+
+            // game line
+            size_t curIdx = lineStart;
+            while (curIdx <= lineEnd) {
+                if (pgn[curIdx] == '*')
+                    return game;
+
+                while (pgn[curIdx] == '{') {
+                    // skip comment
+                    curIdx = pgn.find('}', curIdx);
+                    if (curIdx == std::string::npos) {
+                        std::cerr << "Missing '}' for move comment while reading pgn." << std::endl;
+                        return game;
+                    }
+                    curIdx += 2;
+                }
+                while (pgn[curIdx] == '(') {
+                    // skip comment
+                    curIdx = pgn.find(')', curIdx);
+                    if (curIdx == std::string::npos) {
+                        std::cerr << "Missing ')' for move comment while reading pgn." << std::endl;
+                        return game;
+                    }
+                    curIdx += 2;
+                }
+
+                if (pgn[curIdx] >= '0' && pgn[curIdx] <= '9') {
+                    // we are at a move number -> look for next point
+                    curIdx = pgn.find('.', curIdx);
+                    if (curIdx == std::string::npos)
+                        break;
+                    ++curIdx;
+                    // increment if we're at a space
+                    while (curIdx < pgn.size() && pgn[curIdx] == ' ')
+                        ++curIdx;
+                    // increment if we're at a point
+                    while (curIdx < pgn.size() && pgn[curIdx] == '.')
+                        ++curIdx;
+                }
+                // extract sanMove
+                size_t sanMoveEnd = std::min(pgn.find(' ', curIdx), lineEnd);
+                if (sanMoveEnd > curIdx) {
+                    std::string sanMove = pgn.substr(curIdx, sanMoveEnd-curIdx);
+                    // clean possible ? and ! from string
+                    size_t annotationChar1 = sanMove.find('?');
+                    size_t annotationChar2 = sanMove.find('!');
+                    if (annotationChar1 != std::string::npos || annotationChar2 != std::string::npos)
+                        sanMove = sanMove.substr(0, std::min(annotationChar1, annotationChar2));
+                    game.board->push_san(sanMove);
+                }
+                curIdx = sanMoveEnd+1;
+            }
+        }
+        lineStart = lineEnd+1;
+
+        if (lineStart >= pgn.size())
+            return game;
+    }
+    return game;
+}
+
+
 // binding code
 EMSCRIPTEN_BINDINGS(ffish_js) {
   class_<Board>("Board")
@@ -291,7 +500,8 @@ EMSCRIPTEN_BINDINGS(ffish_js) {
     .function("legalMovesSan", &Board::legal_moves_san)
     .function("numberLegalMoves", &Board::number_legal_moves)
     .function("push", &Board::push)
-    .function("pushSan", &Board::push_san)
+    .function("pushSan", select_overload<bool(std::string)>(&Board::push_san))
+    .function("pushSan", select_overload<bool(std::string, Notation)>(&Board::push_san))
     .function("pop", &Board::pop)
     .function("reset", &Board::reset)
     .function("is960", &Board::is_960)
@@ -308,7 +518,15 @@ EMSCRIPTEN_BINDINGS(ffish_js) {
     .function("gamePly", &Board::game_ply)
     .function("isGameOver", &Board::is_game_over)
     .function("isCheck", &Board::is_check)
-    .function("isBikjang", &Board::is_bikjang);
+    .function("isBikjang", &Board::is_bikjang)
+    .function("moveStack", &Board::move_stack)
+    .function("pushMoves", &Board::push_moves)
+    .function("pushSanMoves", select_overload<void(std::string)>(&Board::push_san_moves))
+    .function("pushSanMoves", select_overload<void(std::string, Notation)>(&Board::push_san_moves));
+  class_<Game>("Game")
+    .function("headerKeys", &Game::header_keys)
+    .function("headers", &Game::headers)
+    .function("mainlineMoves", &Game::mainline_moves);
   // usage: e.g. ffish.Notation.DEFAULT
   enum_<Notation>("Notation")
     .value("DEFAULT", NOTATION_DEFAULT)
@@ -319,10 +537,13 @@ EMSCRIPTEN_BINDINGS(ffish_js) {
     .value("SHOGI_HODGES_NUMBER", NOTATION_SHOGI_HODGES_NUMBER)
     .value("JANGGI", NOTATION_JANGGI)
     .value("XIANGQI_WXF", NOTATION_XIANGQI_WXF);
-  function("info", &info);
-  function("setOption", &set_option<std::string>);
-  function("setOptionInt", &set_option<int>);
-  function("setOptionBool", &set_option<bool>);
+  function("info", &ffish::info);
+  function("setOption", &ffish::set_option<std::string>);
+  function("setOptionInt", &ffish::set_option<int>);
+  function("setOptionBool", &ffish::set_option<bool>);
+  function("readGamePGN", &read_game_pgn);
+  function("variants", &ffish::available_variants);
+  function("loadVariantConfig", &ffish::load_variant_config);
   // TODO: enable to string conversion method
   // .class_function("getStringFromInstance", &Board::get_string_from_instance);
 }
index d742d33..9ddb9ae 100644 (file)
@@ -1189,7 +1189,9 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) {
   assert(is_ok(m));
   assert(&newSt != st);
 
+#ifndef NO_THREADS
   thisThread->nodes.fetch_add(1, std::memory_order_relaxed);
+#endif
   Key k = st->key ^ Zobrist::side;
 
   // Copy some fields of the old state to our new StateInfo object except the
@@ -1284,8 +1286,9 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) {
       // Update material hash key and prefetch access to materialTable
       k ^= Zobrist::psq[captured][capsq];
       st->materialKey ^= Zobrist::psq[captured][pieceCount[captured]];
+#ifndef NO_THREADS
       prefetch(thisThread->materialTable[st->materialKey]);
-
+#endif
       // Reset rule 50 counter
       st->rule50 = 0;
   }
index fc6b40b..19b9826 100644 (file)
@@ -1038,18 +1038,10 @@ void VariantMap::init() {
 }
 
 
-/// VariantMap::parse reads variants from an INI-style configuration file.
+/// VariantMap::parse_istream reads variants from an INI-style configuration input stream.
 
 template <bool DoCheck>
-void VariantMap::parse(std::string path) {
-    if (path.empty() || path == "<empty>")
-        return;
-    std::ifstream file(path);
-    if (!file.is_open())
-    {
-        std::cerr << "Unable to open file " << path << std::endl;
-        return;
-    }
+void VariantMap::parse_istream(std::istream& file) {
     std::string variant, variant_template, key, value, input;
     while (file.peek() != '[' && std::getline(file, input)) {}
 
@@ -1092,7 +1084,6 @@ void VariantMap::parse(std::string path) {
                 delete v;
         }
     }
-    file.close();
     // Clean up temporary variants
     for (std::string tempVar : varsToErase)
     {
@@ -1101,6 +1092,22 @@ void VariantMap::parse(std::string path) {
     }
 }
 
+/// VariantMap::parse reads variants from an INI-style configuration file.
+
+template <bool DoCheck>
+void VariantMap::parse(std::string path) {
+    if (path.empty() || path == "<empty>")
+        return;
+    std::ifstream file(path);
+    if (!file.is_open())
+    {
+        std::cerr << "Unable to open file " << path << std::endl;
+        return;
+    }
+    parse_istream<DoCheck>(file);
+    file.close();
+}
+
 template void VariantMap::parse<true>(std::string path);
 template void VariantMap::parse<false>(std::string path);
 
index 60c9e20..5e22704 100644 (file)
@@ -149,6 +149,7 @@ class VariantMap : public std::map<std::string, const Variant*> {
 public:
   void init();
   template <bool DoCheck> void parse(std::string path);
+  template <bool DoCheck> void parse_istream(std::istream& file);
   void clear_all();
   std::vector<std::string> get_keys();
 
index 443368a..2d370d5 100644 (file)
@@ -15,7 +15,7 @@
 
 The package **ffish.js** is a high performance WebAssembly chess variant library based on [_Fairy-Stockfish_](https://github.com/ianfab/Fairy-Stockfish).
 
-It is available as a [standard module](https://www.npmjs.com/package/ffish) and as an [ES6 module](https://www.npmjs.com/package/ffish-es6).
+It is available as a [standard module](https://www.npmjs.com/package/ffish), as an [ES6 module](https://www.npmjs.com/package/ffish-es6) and aims to have a syntax similar to [python-chess](https://python-chess.readthedocs.io/en/latest/index.html).
 
 ## Install instructions
 
@@ -43,7 +43,7 @@ const ffish = require('ffish');
 ### ES6 module
 
 ```javascript
-import Module from 'ffish';
+import Module from 'ffish-es6';
 let ffish = null;
 
 new Module().then(loadedModule => {
@@ -53,10 +53,44 @@ new Module().then(loadedModule => {
 });
 ```
 
+### Available variants
+
+Show all available variants supported by _Fairy-Stockfish_ and **ffish.js**.
+
+```javascript
+ffish.variants()
+```
+```
+>> 3check 5check ai-wok almost amazon antichess armageddon asean ataxx breakthrough bughouse cambodian\
+capablanca capahouse caparandom centaur chancellor chess chessgi chigorin clobber clobber10 codrus courier\
+crazyhouse dobutsu embassy euroshogi extinction fairy fischerandom gardner giveaway gorogoro gothic grand\
+hoppelpoppel horde janggi janggicasual janggimodern janggitraditional janus jesonmor judkins karouk kinglet\
+kingofthehill knightmate koedem kyotoshogi loop losalamos losers makpong makruk manchu micro mini minishogi\
+minixiangqi modern newzealand nocastle normal placement pocketknight racingkings seirawan shako shatar\
+shatranj shogi shouse sittuyin suicide supply threekings xiangqi
+```
+
+## Custom variants
+
+Fairy-Stockfish also allows defining custom variants by loading a configuration file.
+See e.g. the confiugration for connect4, tictactoe or janggihouse in [variants.ini](https://github.com/ianfab/Fairy-Stockfish/blob/master/src/variants.ini).
+```javascript
+fs = require('fs');
+let configFilePath = './variants.ini';
+ fs.readFile(configFilePath, 'utf8', function (err,data) {
+   if (err) {
+     return console.log(err);
+   }
+   ffish.loadVariantConfig(data)
+   let board = new ffish.Board("tictactoe");
+   board.delete();
+ });
+```
+
 ### Board object
 
 Create a new variant board from its default starting position.
-The even `onRuntimeInitialized` ensures that the wasm file was properly loaded.
+The event `onRuntimeInitialized` ensures that the wasm file was properly loaded.
 
 ```javascript
 ffish['onRuntimeInitialized'] = () => {
@@ -89,14 +123,41 @@ for (var i = 0; i < legalMovesSan.length; i++) {
 }
 ```
 
-For examples for every function see [test.js](https://github.com/ianfab/Fairy-Stockfish/blob/master/tests/js/test.js).
-
 Unfortunately, it is impossible for Emscripten to call the destructors on C++ object.
 Therefore, you need to call `.delete()` to free the heap memory of an object.
 ```javascript
 board.delete();
 ```
 
+## PGN parsing
+
+Read a string from a file and parse it as a single PGN game.
+
+```javascript
+fs = require('fs');
+let pgnFilePath = "data/pgn/kasparov-deep-blue-1997.pgn"
+
+fs.readFile(pgnFilePath, 'utf8', function (err,data) {
+  if (err) {
+    return console.log(err);
+  }
+  game = ffish.readGamePGN(data);
+  console.log(game.headerKeys());
+  console.log(game.headers("White"));
+  console.log(game.mainlineMoves())
+
+  let board = new ffish.Board(game.headers("Variant").toLowerCase());
+  board.pushMoves(game.mainlineMoves());
+
+  let finalFen = board.fen();
+  board.delete();
+  game.delete();
+}
+```
+
+## Remaining features
+
+For an example of each available function see [test.js](https://github.com/ianfab/Fairy-Stockfish/blob/master/tests/js/test.js).
 
 ## Build instuctions
 
@@ -112,14 +173,19 @@ If you want to disable variants with a board greater than 8x8,
 
 The pre-compiled wasm binary is built with `-DLARGEBOARDS`.
 
+It is recommended to set `-s ASSERTIONS=1 -s SAFE_HEAP=1` before running tests.
+
+
 ### Compile as standard module
 
 ```bash
 cd Fairy-Stockfish/src
 ```
 ```bash
-emcc -O3 --bind -s TOTAL_MEMORY=67108864 -s ALLOW_MEMORY_GROWTH=1 \
- -s WASM_MEM_MAX=2147483648 -DLARGEBOARDS -DPRECOMPUTED_MAGICS \
+emcc -O3 --bind -DLARGEBOARDS -DPRECOMPUTED_MAGICS -DNO_THREADS \
+ -s TOTAL_MEMORY=67108864 -s ALLOW_MEMORY_GROWTH=1 -s WASM_MEM_MAX=2147483648 \
+ -s ASSERTIONS=0 -s SAFE_HEAP=0 \
+ -DNO_THREADS -DLARGEBOARDS -DPRECOMPUTED_MAGICS \
 ffishjs.cpp \
 benchmark.cpp \
 bitbase.cpp \
@@ -157,10 +223,10 @@ Some environments such as [vue-js](https://vuejs.org/) may require the library t
 cd Fairy-Stockfish/src
 ```
 ```bash
-emcc -O3 -DLARGEBOARDS --bind \
--s TOTAL_MEMORY=67108864 -s ALLOW_MEMORY_GROWTH=1 \
--s WASM_MEM_MAX=2147483648 -DLARGEBOARDS -DPRECOMPUTED_MAGICS \
--s ENVIRONMENT='web,worker' -s EXPORT_ES6=1 -s MODULARIZE=1 -s USE_ES6_IMPORT_META=0 \
+emcc -O3 --bind -DLARGEBOARDS -DPRECOMPUTED_MAGICS -DNO_THREADS \
+ -s TOTAL_MEMORY=67108864 -s ALLOW_MEMORY_GROWTH=1 -s WASM_MEM_MAX=2147483648 \
+ -s ASSERTIONS=0 -s SAFE_HEAP=0 \
+ -s ENVIRONMENT='web,worker' -s EXPORT_ES6=1 -s MODULARIZE=1 -s USE_ES6_IMPORT_META=0 \
 ffishjs.cpp \
 benchmark.cpp \
 bitbase.cpp \
index ad237c1..12ea650 100644 (file)
@@ -1,6 +1,6 @@
 {
   "name": "ffish",
-  "version": "0.2.0",
+  "version": "0.4.2",
   "description": "A high performance WebAssembly chess variant library based on Fairy-Stockfish",
   "main": "ffish.js",
   "scripts": {
index e92e0ea..ca6331c 100644 (file)
@@ -1,6 +1,8 @@
 before(() => {
   chai = require('chai');
   return new Promise((resolve) => {
+    pgnDir = __dirname + '/../pgn/';
+    srcDir = __dirname + '/../../src/';
     ffish = require('./ffish.js');
     ffish['onRuntimeInitialized'] = () => {
       resolve();
@@ -8,6 +10,22 @@ before(() => {
   });
 });
 
+describe('ffish.loadVariantConfig(config)', function () {
+  it("it loads a custom variant configuration from a string", () => {
+    fs = require('fs');
+    let configFilePath = srcDir + 'variants.ini';
+     fs.readFile(configFilePath, 'utf8', function (err,data) {
+       if (err) {
+         return console.log(err);
+       }
+       ffish.loadVariantConfig(data)
+       let board = new ffish.Board("tictactoe");
+       chai.expect(board.fen()).to.equal("3/3/3[PPPPPpppp] w - - 0 1");
+       board.delete();
+     });
+  });
+});
+
 describe('Board()', function () {
   it("it creates a chess board from the default position", () => {
     const board = new ffish.Board();
@@ -98,7 +116,7 @@ describe('board.push(uciMove)', function () {
   });
 });
 
-describe('board.pushSan()', function () {
+describe('board.pushSan(sanMove)', function () {
   it("it pushes a move in san notation to the board", () => {
     let board = new ffish.Board();
     board.pushSan("e4");
@@ -109,6 +127,17 @@ describe('board.pushSan()', function () {
   });
 });
 
+describe('board.pushSan(sanMove, notation)', function () {
+  it("it pushes a move in san notation to the board", () => {
+    let board = new ffish.Board();
+    board.pushSan("e4", ffish.Notation.SAN);
+    board.pushSan("e5", ffish.Notation.SAN);
+    board.pushSan("Nf3", ffish.Notation.SAN);
+    chai.expect(board.fen()).to.equal("rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2");
+    board.delete();
+  });
+});
+
 describe('board.pop()', function () {
   it("it undos the last move", () => {
     let board = new ffish.Board();
@@ -309,6 +338,48 @@ describe('board.isBikjang()', function () {
   });
 });
 
+describe('board.moveStack()', function () {
+  it("it returns the move stack in UCI notation", () => {
+    let board = new ffish.Board();
+    chai.expect(board.isBikjang()).to.equal(false);
+    board.push("e2e4");
+    board.push("e7e5");
+    board.push("g1f3");
+    chai.expect(board.moveStack()).to.equal("e2e4 e7e5 g1f3");
+    board.setFen("r1bqkb1r/pppp1ppp/2n2n2/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR w KQkq - 4 4");
+    board.pushSan("Qxf7#");
+    chai.expect(board.moveStack()).to.equal("h5f7");
+    board.delete();
+  });
+});
+
+describe('board.pushMoves(uciMoves)', function () {
+  it("it pushes multiple uci moves on the board, passed as a string with ' ' as delimiter", () => {
+    let board = new ffish.Board();
+    board.pushMoves("e2e4 e7e5 g1f3");
+    chai.expect(board.fen()).to.equal("rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2");
+    board.delete();
+  });
+});
+
+describe('board.pushSanMoves(sanMoves)', function () {
+  it("it pushes multiple san moves on the board, passed as a string with ' ' as delimiter", () => {
+    let board = new ffish.Board();
+    board.pushSanMoves("e4 e5 Nf3");
+    chai.expect(board.fen()).to.equal("rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2");
+    board.delete();
+  });
+});
+
+describe('board.pushSanMoves(sanMoves, notation)', function () {
+  it("it pushes multiple san moves on the board, passed as a string with ' ' as delimiter", () => {
+    let board = new ffish.Board();
+    board.pushSanMoves("e4 e5 Nf3", ffish.Notation.SAN);
+    chai.expect(board.fen()).to.equal("rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2");
+    board.delete();
+  });
+});
+
 describe('ffish.info()', function () {
   it("it returns the version of the Fairy-Stockfish binary", () => {
     chai.expect(ffish.info()).to.be.a('string');
@@ -335,3 +406,92 @@ describe('ffish.setOptionBool(name, value)', function () {
     chai.expect(true).to.equal(true);
   });
 });
+
+describe('ffish.readGamePGN(pgn)', function () {
+  it("it reads a pgn string and returns a game object", () => {
+     fs = require('fs');
+     let pgnFiles = ['deep_blue_kasparov_1997.pgn', 'lichess_pgn_2018.12.21_JannLee_vs_CrazyAra.j9eQS4TF.pgn', 'c60_ruy_lopez.pgn']
+     let expectedFens = ["1r6/5kp1/RqQb1p1p/1p1PpP2/1Pp1B3/2P4P/6P1/5K2 b - - 14 45",
+                         "3r2kr/2pb1Q2/4ppp1/3pN2p/1P1P4/3PbP2/P1P3PP/6NK[PPqrrbbnn] b - - 1 37",
+                         "r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3"]
+
+     for (let idx = 0; idx < pgnFiles.length; ++idx) {
+     let pgnFilePath = pgnDir + pgnFiles[idx];
+
+     fs.readFile(pgnFilePath, 'utf8', function (err,data) {
+       if (err) {
+         return console.log(err);
+       }
+       let game = ffish.readGamePGN(data);
+
+       let board = new ffish.Board(game.headers("Variant").toLowerCase());
+       board.pushMoves(game.mainlineMoves());
+       chai.expect(board.fen()).to.equal(expectedFens[idx]);
+       board.delete();
+       game.delete();
+     });
+         }
+  });
+});
+
+describe('game.headerKeys()', function () {
+  it("it returns all available header keys of the loaded game", () => {
+     fs = require('fs');
+     let pgnFile = 'lichess_pgn_2018.12.21_JannLee_vs_CrazyAra.j9eQS4TF.pgn'
+     let pgnFilePath = pgnDir + pgnFile;
+
+     fs.readFile(pgnFilePath, 'utf8', function (err,data) {
+       if (err) {
+         return console.log(err);
+       }
+       let game = ffish.readGamePGN(data);
+       chai.expect(game.headerKeys()).to.equal('Annotator Termination Variant ECO WhiteTitle BlackRatingDiff UTCTime Result WhiteElo Black UTCDate TimeControl BlackElo Event WhiteRatingDiff BlackTitle White Date Opening Site');
+       game.delete();
+     });
+  });
+});
+
+
+describe('game.headers(key)', function () {
+  it("it returns the value for a given header key of a loaded game", () => {
+     fs = require('fs');
+     let pgnFile = 'lichess_pgn_2018.12.21_JannLee_vs_CrazyAra.j9eQS4TF.pgn';
+     let pgnFilePath = pgnDir + pgnFile;
+
+     fs.readFile(pgnFilePath, 'utf8', function (err,data) {
+       if (err) {
+         return console.log(err);
+       }
+       let game = ffish.readGamePGN(data);
+       chai.expect(game.headers("White")).to.equal("JannLee");
+       chai.expect(game.headers("Black")).to.equal("CrazyAra");
+       chai.expect(game.headers("Variant")).to.equal("Crazyhouse");
+       game.delete();
+     });
+  });
+});
+
+describe('game.mainlineMoves()', function () {
+  it("it returns the mainline of the loaded game in UCI notation", () => {
+     fs = require('fs');
+     let pgnFile = 'lichess_pgn_2018.12.21_JannLee_vs_CrazyAra.j9eQS4TF.pgn';
+     let pgnFilePath = pgnDir + pgnFile;
+
+     fs.readFile(pgnFilePath, 'utf8', function (err,data) {
+       if (err) {
+         return console.log(err);
+       }
+       let game = ffish.readGamePGN(data);
+       chai.expect(game.mainlineMoves()).to.equal('e2e4 b8c6 b1c3 g8f6 d2d4 d7d5 e4e5 f6e4 f1b5 a7a6 b5c6 b7c6 g1e2 c8f5 e1g1 e7e6 f2f3 e4c3 b2c3 h7h5 N@e3 N@h4 N@a5 B@d7 e3f5 h4f5 B@b7 N@e3 a5c6 e3d1 c6d8 a8d8 f1d1 Q@b5 b7a6 b5a6 P@d3 N@e3 c1e3 f5e3 P@d6 e3d1 a1d1 B@e3 g1h1 f8d6 e5d6 a6d6 B@b4 d6b4 c3b4 P@f2 Q@f1 R@g1 f1g1 f2g1q d1g1 P@f2 N@g6 f2g1q e2g1 Q@e7 Q@d6 f7g6 d6e7 e8e7 R@f7 e7f7 N@e5 f7g8 N@f6 g7f6 Q@f7');
+       game.delete();
+     });
+  });
+});
+
+describe('ffish.variants()', function () {
+  it("it returns all currently available variants", () => {
+    chai.expect(ffish.variants().includes("chess")).to.equal(true);
+    chai.expect(ffish.variants().includes("crazyhouse")).to.equal(true);
+    chai.expect(ffish.variants().includes("janggi")).to.equal(true);
+  });
+});
diff --git a/tests/pgn/c60_ruy_lopez.pgn b/tests/pgn/c60_ruy_lopez.pgn
new file mode 100644 (file)
index 0000000..4ec04a0
--- /dev/null
@@ -0,0 +1,10 @@
+[Event "?"]
+[Site "?"]
+[Date "2020.09.24"]
+[Round "?"]
+[White "White"]
+[Black "Black"]
+[Result "*"]
+
+1. e4 e5 2. Nf3 Nc6 3. Bb5 *
+
diff --git a/tests/pgn/deep_blue_kasparov_1997.pgn b/tests/pgn/deep_blue_kasparov_1997.pgn
new file mode 100644 (file)
index 0000000..ce5b2eb
--- /dev/null
@@ -0,0 +1,22 @@
+[Event "IBM Man-Machine"]
+[Site "New York, NY USA"]
+[Date "1997.05.04"]
+[EventDate "?"]
+[Round "2"]
+[Result "1-0"]
+[White "Deep Blue (Computer)"]
+[Black "Garry Kasparov"]
+[ECO "C93"]
+[WhiteElo "?"]
+[BlackElo "?"]
+[PlyCount "89"]
+
+1.e4 e5 2.Nf3 Nc6 3.Bb5 a6 4.Ba4 Nf6 5.O-O Be7 6.Re1 b5 7.Bb3
+d6 8.c3 O-O 9.h3 h6 10.d4 Re8 11.Nbd2 Bf8 12.Nf1 Bd7 13.Ng3
+Na5 14.Bc2 c5 15.b3 Nc6 16.d5 Ne7 17.Be3 Ng6 18.Qd2 Nh7 19.a4
+Nh4 20.Nxh4 Qxh4 21.Qe2 Qd8 22.b4 Qc7 23.Rec1 c4 24.Ra3 Rec8
+25.Rca1 Qd8 26.f4 Nf6 27.fxe5 dxe5 28.Qf1 Ne8 29.Qf2 Nd6
+30.Bb6 Qe8 31.R3a2 Be7 32.Bc5 Bf8 33.Nf5 Bxf5 34.exf5 f6
+35.Bxd6 Bxd6 36.axb5 axb5 37.Be4 Rxa2 38.Qxa2 Qd7 39.Qa7 Rc7
+40.Qb6 Rb7 41.Ra8+ Kf7 42.Qa6 Qc7 43.Qc6 Qb6+ 44.Kf1 Rb8
+45.Ra6 1-0\r
\ No newline at end of file
diff --git a/tests/pgn/lichess_pgn_2018.12.21_JannLee_vs_CrazyAra.j9eQS4TF.pgn b/tests/pgn/lichess_pgn_2018.12.21_JannLee_vs_CrazyAra.j9eQS4TF.pgn
new file mode 100644 (file)
index 0000000..74115e0
--- /dev/null
@@ -0,0 +1,24 @@
+[Event "Rated Crazyhouse game"]
+[Site "https://lichess.org/j9eQS4TF"]
+[Date "2018.12.21"]
+[White "JannLee"]
+[Black "CrazyAra"]
+[Result "1-0"]
+[UTCDate "2018.12.21"]
+[UTCTime "18:33:01"]
+[WhiteElo "2641"]
+[BlackElo "2610"]
+[WhiteRatingDiff "+6"]
+[BlackRatingDiff "-13"]
+[WhiteTitle "LM"]
+[BlackTitle "BOT"]
+[Variant "Crazyhouse"]
+[TimeControl "300+15"]
+[ECO "B00"]
+[Opening "Nimzowitsch Defense: Scandinavian Variation, Bogoljubov Variation, Vehre Variation"]
+[Termination "Normal"]
+[Annotator "lichess.org"]
+
+1. e4 { [%eval 1.52] [%clk 0:05:00] } 1... Nc6 { [%eval 0.77] [%clk 0:05:00] } 2. Nc3 { [%eval 0.66] [%clk 0:05:11] } 2... Nf6?! { (0.66 → 1.19) Inaccuracy. e5 was best. } { [%eval 1.19] [%clk 0:05:08] } (2... e5 3. Nf3 Nf6 4. Bc4 Bc5 5. O-O d6 6. d3 O-O 7. Ng5) 3. d4 { [%eval 0.67] [%clk 0:05:11] } 3... d5 { [%eval 0.84] [%clk 0:05:08] } { B00 Nimzowitsch Defense: Scandinavian Variation, Bogoljubov Variation, Vehre Variation } 4. e5 { [%eval 0.61] [%clk 0:05:24] } 4... Ne4 { [%eval 0.75] [%clk 0:05:15] } 5. Bb5 { [%eval 2.46] [%clk 0:05:26] } 5... a6 { [%eval 3.05] [%clk 0:05:07] } 6. Bxc6+ { [%eval 2.5] [%clk 0:05:28] } 6... bxc6 { [%eval 3.2] [%clk 0:05:14] } 7. Nge2 { [%eval 0.48] [%clk 0:05:17] } 7... Bf5?? { (0.48 → 2.75) Blunder. Bg4 was best. } { [%eval 2.75] [%clk 0:05:03] } (7... Bg4 8. O-O Nxc3 9. bxc3 B@h3 10. f3 Bxg2 11. fxg4 Bxf1 12. Qxf1) 8. O-O { [%eval 3.0] [%clk 0:05:20] } 8... e6 { [%eval 2.65] [%clk 0:04:53] } 9. f3 { [%eval 2.62] [%clk 0:05:17] } 9... Nxc3 { [%eval 2.61] [%clk 0:05:01] } 10. bxc3 { [%eval 2.55] [%clk 0:05:32] } 10... h5?! { (2.55 → 3.85) Inaccuracy. B@a4 was best. } { [%eval 3.85] [%clk 0:04:50] } (10... B@a4 11. N@e3) 11. N@e3 { [%eval 3.49] [%clk 0:03:36] } 11... N@h4? { (3.49 → 5.60) Mistake. Bh7 was best. } { [%eval 5.6] [%clk 0:04:38] } (11... Bh7 12. Rb1 B@a4 13. Nf4 h4 14. Rb7 Be7 15. N@h5 Rg8 16. c4 dxc4 17. Nxc4 Baxc2 18. Qd2) 12. N@a5?? { (5.60 → 2.40) Blunder. Nxf5 was best. } { [%eval 2.4] [%clk 0:03:09] } (12. Nxf5 Nxf5 13. B@b7 B@a4 14. N@a5 N@e3 15. Bxe3 Nxe3 16. Qd2 Nxc2 17. Bxa8 Qxa8 18. Rab1 P@b4) 12... B@d7?? { (2.40 → 4.92) Blunder. Nxg2 was best. } { [%eval 4.92] [%clk 0:04:32] } (12... Nxg2 13. Nxg2 P@d7 14. Rb1 h4 15. Ngf4 B@g5 16. Rf2 Bfe7 17. Rg2 Kf8 18. Bd2 Kg8 19. N@h5) 13. Nxf5 { [%eval 3.65] [%clk 0:03:20] } 13... Nxf5 { [%eval 5.1] [%clk 0:04:39] } 14. B@b7 { [%eval 4.07] [%clk 0:03:33] } 14... N@e3 { [%eval 4.93] [%clk 0:04:26] } 15. Nxc6?? { (4.93 → 1.71) Blunder. Bxe3 was best. } { [%eval 1.71] [%clk 0:03:14] } (15. Bxe3 Nxe3) 15... Nxd1 { [%eval 3.35] [%clk 0:04:16] } 16. Nxd8 { [%eval 1.06] [%clk 0:03:26] } 16... Rxd8?? { (1.06 → 4.55) Blunder. Nxd4 was best. } { [%eval 4.55] [%clk 0:04:24] } (16... Nxd4 17. cxd4 P@f2+ 18. Rxf2 Nxf2 19. Kxf2 Rxd8 20. Bd2 Q@h4+ 21. Q@g3 Qxg3+ 22. hxg3 R@h2 23. N@f4) 17. Rxd1 { [%eval 4.69] [%clk 0:03:28] } 17... Q@b5 { [%eval 5.71] [%clk 0:04:13] } 18. Bxa6?? { (5.71 → 1.41) Blunder. P@g6 was best. } { [%eval 1.41] [%clk 0:03:21] } (18. P@g6 fxg6 19. Bxa6 Qxa6 20. P@f7+ Ke7 21. Nf4 Bb5 22. c4 Bxc4 23. Q@g5+ P@f6 24. exf6+ gxf6) 18... Qxa6 { [%eval 1.22] [%clk 0:04:03] } 19. P@d3?? { (1.22 → -2.85) Blunder. P@g6 was best. } { [%eval -2.85] [%clk 0:03:31] } (19. P@g6 B@g8) 19... N@e3?? { (-2.85 → 0.05) Blunder. h4 was best. } { [%eval 0.05] [%clk 0:03:55] } (19... h4) 20. Bxe3 { [%eval -0.32] [%clk 0:03:37] } 20... Nxe3 { [%eval 0.0] [%clk 0:04:01] } 21. P@d6?? { (0.00 → -2.41) Blunder. N@g6 was best. } { [%eval -2.41] [%clk 0:01:29] } (21. N@g6 Nxd1 22. Rxd1 B@e3+ 23. P@f2 fxg6 24. fxe3 B@h4 25. B@g3 Bxg3 26. hxg3 B@g5 27. B@f4 Bfe7) 21... Nxd1?? { (-2.41 → Mate in 3) Checkmate is now unavoidable. Bxd6 was best. } { [%eval #3] [%clk 0:03:55] } (21... Bxd6 22. exd6) 22. Rxd1?? { (Mate in 3 → -1.88) Lost forced checkmate sequence. N@f6+ was best. } { [%eval -1.88] [%clk 0:01:44] } (22. N@f6+ gxf6 23. N@g7+ Bxg7 24. Q@e7#) 22... B@e3+?? { (-1.88 → 0.00) Blunder. Bxd6 was best. } { [%eval 0.0] [%clk 0:04:02] } (22... Bxd6 23. exd6 Qxd6 24. B@e3 B@h4 25. N@e5 O-O 26. g3 B@g5 27. N@f4 Qxe5 28. dxe5 N@h3+ 29. Nxh3) 23. Kh1 { [%eval -0.42] [%clk 0:01:51] } 23... Bxd6 { [%eval 0.07] [%clk 0:03:55] } 24. exd6 { [%eval 0.55] [%clk 0:02:02] } 24... Qxd6 { [%eval 1.38] [%clk 0:04:01] } 25. B@b4?? { (1.38 → -8.92) Blunder. B@e5 was best. } { [%eval -8.92] [%clk 0:01:08] } (25. B@e5 B@e7) 25... Qxb4?? { (-8.92 → 0.53) Blunder. P@h3 was best. } { [%eval 0.53] [%clk 0:03:55] } (25... P@h3) 26. cxb4 { [%eval -0.77] [%clk 0:01:21] } 26... P@f2?? { (-0.77 → 4.95) Blunder. B@f6 was best. } { [%eval 4.95] [%clk 0:03:49] } (26... B@f6 27. Q@g3 B@d6 28. N@e5 P@h3 29. Q@f1 Bxd4 30. Nxf7 Kxf7 31. P@g6+ Kf8 32. Qxh3 N@f2+ 33. Qxf2) 27. Q@f1?? { (4.95 → -0.96) Blunder. N@g6 was best. } { [%eval -0.96] [%clk 0:00:19] } (27. N@g6) 27... R@g1+?? { (-0.96 → 8.81) Blunder. B@f6 was best. } { [%eval 8.81] [%clk 0:03:42] } (27... B@f6 28. N@h3 B@h4 29. N@e5 Bxe5 30. dxe5 N@f5 31. Nxf2 Bexf2 32. P@h7 Kf8 33. N@h3 Be3 34. B@g1) 28. Qxg1?? { (8.81 → 0.00) Blunder. Nxg1 was best. } { [%eval 0.0] [%clk 0:00:23] } (28. Nxg1) 28... fxg1=Q+ { [%eval 0.0] [%clk 0:03:49] } 29. Rxg1 { [%eval 0.07] [%clk 0:00:37] } 29... P@f2 { [%eval 0.58] [%clk 0:03:30] } 30. N@g6?! { (0.58 → 0.00) Inaccuracy. Rf1 was best. } { [%eval 0.0] [%clk 0:00:27] } (30. Rf1 B@d6) 30... fxg1=Q+?? { (0.00 → 6.00) Blunder. fxg6 was best. } { [%eval 6.0] [%clk 0:03:34] } (30... fxg6 31. P@f7+ Kf8 32. R@g8+ Ke7 33. f8=Q+ Rxf8 34. Rxg7+ P@f7 35. P@f6+ Kxf6 36. Q@e5+ Ke7 37. Qxe3) 31. Nxg1 { [%eval 4.02] [%clk 0:00:41] } 31... Q@e7? { (4.02 → 8.29) Mistake. B@e7 was best. } { [%eval 8.29] [%clk 0:03:18] } (31... B@e7 32. Q@f6) 32. Q@d6?? { (8.29 → -5.15) Blunder. Q@f6 was best. } { [%eval -5.15] [%clk 0:00:27] } (32. Q@f6 B@g5) 32... fxg6?? { (-5.15 → Mate in 6) Checkmate is now unavoidable. B@f6 was best. } { [%eval #6] [%clk 0:03:10] } (32... B@f6 33. Qxe7+ Bxe7 34. Q@e1 Bxg1 35. Qxg1 R@a1 36. B@e1 Q@e2 37. R@f1 fxg6 38. N@e5 N@f7 39. Nxf7) 33. Qxe7+ { [%eval #5] [%clk 0:00:40] } 33... Kxe7 { [%eval #5] [%clk 0:03:25] } 34. R@f7+ { [%eval #4] [%clk 0:00:54] } 34... Kxf7 { [%eval #4] [%clk 0:03:35] } 35. N@e5+ { [%eval #3] [%clk 0:01:08] } 35... Kg8 { [%eval #2] [%clk 0:03:30] } 36. N@f6+ { [%eval #1] [%clk 0:01:19] } 36... gxf6 { [%eval #1] [%clk 0:03:42] } 37. Q@f7# { [%clk 0:01:18] } { White wins by checkmate. } 1-0
+
+