From 911b07e8f00af078cf6625b1585bf9926bfef3bf Mon Sep 17 00:00:00 2001 From: QueensGambit Date: Wed, 30 Sep 2020 10:40:06 +0200 Subject: [PATCH] Updated ffish.js to 0.4.2 (Closes #185) + 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 | 255 ++++++++++++++++++-- src/position.cpp | 5 +- src/variant.cpp | 29 ++- src/variant.h | 1 + tests/js/README.md | 88 ++++++- tests/js/package.json | 2 +- tests/js/test.js | 162 ++++++++++++- tests/pgn/c60_ruy_lopez.pgn | 10 + tests/pgn/deep_blue_kasparov_1997.pgn | 22 ++ ...pgn_2018.12.21_JannLee_vs_CrazyAra.j9eQS4TF.pgn | 24 ++ 10 files changed, 556 insertions(+), 42 deletions(-) create mode 100644 tests/pgn/c60_ruy_lopez.pgn create mode 100644 tests/pgn/deep_blue_kasparov_1997.pgn create mode 100644 tests/pgn/lichess_pgn_2018.12.21_JannLee_vs_CrazyAra.j9eQS4TF.pgn diff --git a/src/ffishjs.cpp b/src/ffishjs.cpp index 462c65a..5f8fe4e 100644 --- a/src/ffishjs.cpp +++ b/src/ffishjs.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include "misc.h" #include "types.h" @@ -40,16 +41,16 @@ 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(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 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(ss); + Options["UCI_Variant"].set_combo(variants.get_keys()); + Board::sfInitialized = true; +} +} + +class Game { + private: + std::unordered_map header; + std::unique_ptr 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(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") @@ -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(&Board::push_san)) + .function("pushSan", select_overload(&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(&Board::push_san_moves)) + .function("pushSanMoves", select_overload(&Board::push_san_moves)); + class_("Game") + .function("headerKeys", &Game::header_keys) + .function("headers", &Game::headers) + .function("mainlineMoves", &Game::mainline_moves); // usage: e.g. ffish.Notation.DEFAULT enum_("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); - function("setOptionInt", &set_option); - function("setOptionBool", &set_option); + function("info", &ffish::info); + function("setOption", &ffish::set_option); + function("setOptionInt", &ffish::set_option); + function("setOptionBool", &ffish::set_option); + 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); } diff --git a/src/position.cpp b/src/position.cpp index d742d33..9ddb9ae 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -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; } diff --git a/src/variant.cpp b/src/variant.cpp index fc6b40b..19b9826 100644 --- a/src/variant.cpp +++ b/src/variant.cpp @@ -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 -void VariantMap::parse(std::string path) { - if (path.empty() || path == "") - 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 +void VariantMap::parse(std::string path) { + if (path.empty() || path == "") + return; + std::ifstream file(path); + if (!file.is_open()) + { + std::cerr << "Unable to open file " << path << std::endl; + return; + } + parse_istream(file); + file.close(); +} + template void VariantMap::parse(std::string path); template void VariantMap::parse(std::string path); diff --git a/src/variant.h b/src/variant.h index 60c9e20..5e22704 100644 --- a/src/variant.h +++ b/src/variant.h @@ -149,6 +149,7 @@ class VariantMap : public std::map { public: void init(); template void parse(std::string path); + template void parse_istream(std::istream& file); void clear_all(); std::vector get_keys(); diff --git a/tests/js/README.md b/tests/js/README.md index 443368a..2d370d5 100644 --- a/tests/js/README.md +++ b/tests/js/README.md @@ -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 \ diff --git a/tests/js/package.json b/tests/js/package.json index ad237c1..12ea650 100644 --- a/tests/js/package.json +++ b/tests/js/package.json @@ -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": { diff --git a/tests/js/test.js b/tests/js/test.js index e92e0ea..ca6331c 100644 --- a/tests/js/test.js +++ b/tests/js/test.js @@ -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 index 0000000..4ec04a0 --- /dev/null +++ b/tests/pgn/c60_ruy_lopez.pgn @@ -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 index 0000000..ce5b2eb --- /dev/null +++ b/tests/pgn/deep_blue_kasparov_1997.pgn @@ -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 \ 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 index 0000000..74115e0 --- /dev/null +++ b/tests/pgn/lichess_pgn_2018.12.21_JannLee_vs_CrazyAra.j9eQS4TF.pgn @@ -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 + + -- 1.7.0.4