#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:
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() {
}
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);
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) {
// }
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")
.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)
.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)
.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);
}
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
// 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;
}
}
-/// 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)) {}
delete v;
}
}
- file.close();
// Clean up temporary variants
for (std::string tempVar : varsToErase)
{
}
}
+/// 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);
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();
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
### ES6 module
```javascript
-import Module from 'ffish';
+import Module from 'ffish-es6';
let ffish = null;
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'] = () => {
}
```
-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
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 \
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 \
{
"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": {
before(() => {
chai = require('chai');
return new Promise((resolve) => {
+ pgnDir = __dirname + '/../pgn/';
+ srcDir = __dirname + '/../../src/';
ffish = require('./ffish.js');
ffish['onRuntimeInitialized'] = () => {
resolve();
});
});
+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();
});
});
-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");
});
});
+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();
});
});
+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');
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);
+ });
+});
--- /dev/null
+[Event "?"]
+[Site "?"]
+[Date "2020.09.24"]
+[Round "?"]
+[White "White"]
+[Black "Black"]
+[Result "*"]
+
+1. e4 e5 2. Nf3 Nc6 3. Bb5 *
+
--- /dev/null
+[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
--- /dev/null
+[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
+
+