Expose additional game-end functions in ffish.js (#362)
authorTerry Hearst <thearst3rd@gmail.com>
Fri, 8 Oct 2021 13:16:39 +0000 (09:16 -0400)
committerGitHub <noreply@github.com>
Fri, 8 Oct 2021 13:16:39 +0000 (15:16 +0200)
src/apiutil.h
src/ffishjs.cpp
tests/js/README.md
tests/js/package.json
tests/js/test.js

index 6d7b883..97e3fe0 100644 (file)
@@ -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 {
index b6defdd..54d8761 100644 (file)
@@ -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<LEGAL>(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<LEGAL>(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<LEGAL>(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<bool() const>(&Board::is_game_over))
+    .function("isGameOver", select_overload<bool(bool) const>(&Board::is_game_over))
+    .function("result", select_overload<std::string() const>(&Board::result))
+    .function("result", select_overload<std::string(bool) const>(&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>("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<std::string>);
   function("setOptionInt", &ffish::set_option<int>);
index ea58b1f..24cd932 100644 (file)
@@ -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
index a547115..c35f056 100644 (file)
@@ -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": {
index 6c60626..af31857 100644 (file)
@@ -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();