From ed95c275280e371286c69979993ef6fecab9abe8 Mon Sep 17 00:00:00 2001 From: Terry Hearst Date: Fri, 8 Oct 2021 09:16:39 -0400 Subject: [PATCH] Expose additional game-end functions in ffish.js (#362) --- src/apiutil.h | 10 ++++ src/ffishjs.cpp | 73 ++++++++++++++++++++++++++-- tests/js/README.md | 10 ++++ tests/js/package.json | 2 +- tests/js/test.js | 127 ++++++++++++++++++++++++++++++++++++++++++++++++- 5 files changed, 216 insertions(+), 6 deletions(-) diff --git a/src/apiutil.h b/src/apiutil.h index 6d7b883..97e3fe0 100644 --- a/src/apiutil.h +++ b/src/apiutil.h @@ -54,6 +54,16 @@ inline Notation default_notation(const Variant* v) { return NOTATION_SAN; } +enum Termination { + ONGOING, + CHECKMATE, + STALEMATE, + INSUFFICIENT_MATERIAL, + N_MOVE_RULE, + N_FOLD_REPETITION, + VARIANT_END, +}; + namespace SAN { enum Disambiguation { diff --git a/src/ffishjs.cpp b/src/ffishjs.cpp index b6defdd..54d8761 100644 --- a/src/ffishjs.cpp +++ b/src/ffishjs.cpp @@ -267,10 +267,61 @@ public: return pos.game_ply(); } + bool has_insufficient_material(bool turn) const { + return Stockfish::has_insufficient_material(turn ? WHITE : BLACK, pos); + } + + bool is_insufficient_material() const { + return Stockfish::has_insufficient_material(WHITE, pos) && Stockfish::has_insufficient_material(BLACK, pos); + } + bool is_game_over() const { - for (const ExtMove& move: MoveList(pos)) - return false; - return true; + return is_game_over(false); + } + + bool is_game_over(bool claim_draw) const { + if (is_insufficient_material()) + return true; + if (claim_draw && pos.is_optional_game_end()) + return true; + return MoveList(pos).size() == 0; + } + + std::string result() const { + return result(false); + } + + std::string result(bool claim_draw) const { + Value result; + bool gameEnd = pos.is_immediate_game_end(result); + if (!gameEnd) { + if (is_insufficient_material()) { + gameEnd = true; + result = VALUE_DRAW; + } + } + if (!gameEnd && MoveList(pos).size() == 0) { + gameEnd = true; + result = pos.checkers() ? pos.checkmate_value() : pos.stalemate_value(); + } + if (!gameEnd && claim_draw) + gameEnd = pos.is_optional_game_end(result); + + if (!gameEnd) + return "*"; + if (result == 0) { + if (pos.material_counting()) + result = pos.material_counting_result(); + + if (result == 0) + return "1/2-1/2"; + } + if (pos.side_to_move() == BLACK) + result = -result; + if (result > 0) + return "1-0"; + else + return "0-1"; } bool is_check() const { @@ -631,7 +682,12 @@ EMSCRIPTEN_BINDINGS(ffish_js) { .function("fullmoveNumber", &Board::fullmove_number) .function("halfmoveClock", &Board::halfmove_clock) .function("gamePly", &Board::game_ply) - .function("isGameOver", &Board::is_game_over) + .function("hasInsufficientMaterial", &Board::has_insufficient_material) + .function("isInsufficientMaterial", &Board::is_insufficient_material) + .function("isGameOver", select_overload(&Board::is_game_over)) + .function("isGameOver", select_overload(&Board::is_game_over)) + .function("result", select_overload(&Board::result)) + .function("result", select_overload(&Board::result)) .function("isCheck", &Board::is_check) .function("isBikjang", &Board::is_bikjang) .function("moveStack", &Board::move_stack) @@ -656,6 +712,15 @@ EMSCRIPTEN_BINDINGS(ffish_js) { .value("SHOGI_HODGES_NUMBER", NOTATION_SHOGI_HODGES_NUMBER) .value("JANGGI", NOTATION_JANGGI) .value("XIANGQI_WXF", NOTATION_XIANGQI_WXF); + // usage: e.g. ffish.Termination.CHECKMATE + enum_("Termination") + .value("ONGOING", ONGOING) + .value("CHECKMATE", CHECKMATE) + .value("STALEMATE", STALEMATE) + .value("INSUFFICIENT_MATERIAL", INSUFFICIENT_MATERIAL) + .value("N_MOVE_RULE", N_MOVE_RULE) + .value("N_FOLD_REPETITION", N_FOLD_REPETITION) + .value("VARIANT_END", VARIANT_END); function("info", &ffish::info); function("setOption", &ffish::set_option); function("setOptionInt", &ffish::set_option); diff --git a/tests/js/README.md b/tests/js/README.md index ea58b1f..24cd932 100644 --- a/tests/js/README.md +++ b/tests/js/README.md @@ -278,3 +278,13 @@ npm install ```bash node index.js ``` + +## Example Projects + +### ffish-test + +A simple toy website which demonstrates the core functionality of ffish.js and [chessgroundx](https://github.com/gbtami/chessgroundx). + +Source code: https://github.com/thearst3rd/ffish-test + +See it deployed at: https://ffish-test.herokuapp.com diff --git a/tests/js/package.json b/tests/js/package.json index a547115..c35f056 100644 --- a/tests/js/package.json +++ b/tests/js/package.json @@ -1,6 +1,6 @@ { "name": "ffish", - "version": "0.6.2", + "version": "0.6.3", "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 6c60626..af31857 100644 --- a/tests/js/test.js +++ b/tests/js/test.js @@ -330,17 +330,142 @@ describe('board.gamePly()', function () { }); }); +describe('board.hasInsufficientMaterial(side)', function () { + it("it returns if the given side has insufficient mating material", () => { + let board = new ffish.Board(); + chai.expect(board.hasInsufficientMaterial(true)).to.equal(false); + chai.expect(board.hasInsufficientMaterial(false)).to.equal(false); + board.setFen("8/5k2/8/8/8/2K5/6R1/8 w - - 0 1"); + chai.expect(board.hasInsufficientMaterial(true)).to.equal(false); + chai.expect(board.hasInsufficientMaterial(false)).to.equal(true); + board.setFen("8/5k2/8/8/8/2K5/6q1/8 w - - 0 1"); + chai.expect(board.hasInsufficientMaterial(true)).to.equal(true); + chai.expect(board.hasInsufficientMaterial(false)).to.equal(false); + board.setFen("8/5k2/8/8/8/2K5/6B1/8 w - - 0 1"); + chai.expect(board.hasInsufficientMaterial(true)).to.equal(true); + chai.expect(board.hasInsufficientMaterial(false)).to.equal(true); + board.delete(); + }); +}); + +describe('board.isInsufficientMaterial()', function () { + it("it returns if the game is drawn due to insufficient material", () => { + let board = new ffish.Board(); + chai.expect(board.isInsufficientMaterial()).to.equal(false); + board.setFen("8/5k2/8/8/8/2K5/6R1/8 w - - 0 1"); + chai.expect(board.isInsufficientMaterial()).to.equal(false); + board.setFen("8/5k2/8/8/8/2K5/6q1/8 w - - 0 1"); + chai.expect(board.isInsufficientMaterial()).to.equal(false); + board.setFen("8/5k2/8/8/8/2K5/6B1/8 w - - 0 1"); + chai.expect(board.isInsufficientMaterial()).to.equal(true); + board.delete(); + }); +}); + describe('board.isGameOver()', function () { - it("it checks if the game is over based on the number of legal moves", () => { + it("it checks if the game is over", () => { + // No legal moves let board = new ffish.Board(); chai.expect(board.isGameOver()).to.equal(false); board.setFen("r1bqkb1r/pppp1ppp/2n2n2/4p2Q/2B1P3/8/PPPP1PPP/RNB1K1NR w KQkq - 4 4"); board.pushSan("Qxf7#"); chai.expect(board.isGameOver()).to.equal(true); + + // Insufficient material + board.setFen("3Rk3/8/8/8/8/8/2N5/3K4 b - - 0 1"); + chai.expect(board.isGameOver()).to.equal(false); + board.pushSan("Kxd8"); + chai.expect(board.isGameOver()).to.equal(true); + + // Optional draw claimed + board.reset(); + board.pushSanMoves("Nf3 Nc6 Ng1 Nb8 Nf3 Nc6 Ng1"); + chai.expect(board.isGameOver(false)).to.equal(false); + chai.expect(board.isGameOver(true)).to.equal(false); + board.pushSan("Nb8"); + chai.expect(board.isGameOver(false)).to.equal(false); + chai.expect(board.isGameOver(true)).to.equal(true); board.delete(); }); }); +describe('board.result()', function () { + it("it returns a string representing the winner of the game", () => { + // Scholar's mate (win for white) + let board = new ffish.Board(); + chai.expect(board.result()).to.equal("*"); + board.pushSanMoves("e4 e5 Bc4 Nc6 Qh5 Nf6"); + chai.expect(board.result()).to.equal("*"); + board.pushSan("Qxf7#"); + chai.expect(board.result()).to.equal("1-0"); + + // Fool's mate (win for black) + board.reset(); + board.pushSanMoves("f3 e5 g4"); + chai.expect(board.result()).to.equal("*"); + board.pushSan("Qh4#"); + chai.expect(board.result()).to.equal("0-1"); + + // Stalemate + board.setFen("2Q2bnr/4p1pq/5pkr/7p/7P/4P3/PPPP1PP1/RNB1KBNR w KQ - 1 10"); + chai.expect(board.result()).to.equal("*"); + board.pushSan("Qe6"); + chai.expect(board.result()).to.equal("1/2-1/2"); + + // Draw claimed by n-fold repetition + board.reset(); + board.pushSanMoves("Nf3 Nc6 Ng1 Nb8 Nf3 Nc6 Ng1"); + chai.expect(board.result(false)).to.equal("*"); + chai.expect(board.result(true)).to.equal("*"); + board.pushSan("Nb8"); + chai.expect(board.result(false)).to.equal("*"); + chai.expect(board.result(true)).to.equal("1/2-1/2"); + + // Draw claimed by n-move rule + board.setFen("rnbqkbn1/ppppppp1/6r1/7p/2R4P/8/PPPPPPP1/RNBQKBN1 b Qq - 99 51"); + chai.expect(board.result(false)).to.equal("*"); + chai.expect(board.result(true)).to.equal("*"); + board.pushSan("Rh6"); + chai.expect(board.result(false)).to.equal("*"); + chai.expect(board.result(true)).to.equal("1/2-1/2"); + + // Insufficient material + board.setFen("3Rk3/8/8/8/8/8/2N5/3K4 b - - 0 1"); + chai.expect(board.result()).to.equal("*"); + board.pushSan("Kxd8"); + chai.expect(board.result()).to.equal("1/2-1/2"); + + // Insufficient material with material counting - black draw odds (armageddon) + board.delete(); + board = new ffish.Board("armageddon"); + board.setFen("3Rk3/8/8/8/8/8/2N5/3K4 b - - 0 1"); + chai.expect(board.result()).to.equal("*"); + board.pushSan("Kxd8"); + chai.expect(board.result()).to.equal("0-1"); + + // Stalemate with material counting - black draw odds (armageddon) + board.setFen("2Q2bnr/4p1pq/5pkr/7p/7P/4P3/PPPP1PP1/RNB1KBNR w KQ - 1 10"); + chai.expect(board.result()).to.equal("*"); + board.pushSan("Qe6"); + chai.expect(board.result()).to.equal("0-1"); + + // Atomic chess exploded king (variant ending) + board.delete(); + board = new ffish.Board("atomic"); + board.pushMoves("e2e4 e7e5 d1h5 a7a6"); + chai.expect(board.result()).to.equal("*"); + board.push("h5f7"); + chai.expect(board.result()).to.equal("1-0"); + + // Exploded king AND insufficient material - exploded king takes priority + board.setFen("3qk3/8/8/8/8/8/8/3QK3 w - - 0 1"); + chai.expect(board.result()).to.equal("*"); + board.push("d1d8"); + chai.expect(board.result()).to.equal("1-0"); + board.delete(); + }) +}) + describe('board.isCheck()', function () { it("it checks if a player is in check", () => { let board = new ffish.Board(); -- 1.7.0.4