Add Makruk, Shako and Shogi + a few fixes
[vchess.git] / client / src / variants / Shako.js
diff --git a/client/src/variants/Shako.js b/client/src/variants/Shako.js
new file mode 100644 (file)
index 0000000..ee694d0
--- /dev/null
@@ -0,0 +1,340 @@
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { randInt, sample } from "@/utils/alea";
+
+export class ShakoRules extends ChessRules {
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      {
+        initShift: { w: 2, b: 2 },
+        promotions:
+          ChessRules.PawnSpecs.promotions.concat([V.ELEPHANT, V.CANNON])
+      }
+    );
+  }
+
+  static get ELEPHANT() {
+    return "e";
+  }
+
+  static get CANNON() {
+    return "c";
+  }
+
+  static get PIECES() {
+    return ChessRules.PIECES.concat([V.ELEPHANT, V.CANNON]);
+  }
+
+  getPpath(b) {
+    const prefix = [V.ELEPHANT, V.CANNON].includes(b[1]) ? "Shako/" : "";
+    return prefix + b;
+  }
+
+  static get steps() {
+    return Object.assign(
+      {},
+      ChessRules.steps,
+      {
+        e: [
+          [-1, -1],
+          [-1, 1],
+          [1, -1],
+          [1, 1],
+          [-2, -2],
+          [-2, 2],
+          [2, -2],
+          [2, 2]
+        ]
+      }
+    );
+  }
+
+  static get size() {
+    return { x: 10, y: 10};
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    switch (this.getPiece(x, y)) {
+      case V.ELEPHANT:
+        return this.getPotentialElephantMoves([x, y]);
+      case V.CANNON:
+        return this.getPotentialCannonMoves([x, y]);
+      default:
+        return super.getPotentialMovesFrom([x, y]);
+    }
+  }
+
+  getPotentialElephantMoves([x, y]) {
+    return this.getSlideNJumpMoves([x, y], V.steps[V.ELEPHANT], "oneStep");
+  }
+
+  getPotentialCannonMoves([x, y]) {
+    const oppCol = V.GetOppCol(this.turn);
+    let moves = [];
+    // Look in every direction until an obstacle (to jump) is met
+    for (const step of V.steps[V.ROOK]) {
+      let i = x + step[0];
+      let j = y + step[1];
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        moves.push(this.getBasicMove([x, y], [i, j]));
+        i += step[0];
+        j += step[1];
+      }
+      // Then, search for an enemy
+      i += step[0];
+      j += step[1];
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        i += step[0];
+        j += step[1];
+      }
+      if (V.OnBoard(i, j) && this.getColor(i, j) == oppCol)
+        moves.push(this.getBasicMove([x, y], [i, j]));
+    }
+    return moves;
+  }
+
+  getCastleMoves([x, y]) {
+    const c = this.getColor(x, y);
+    if (x != (c == "w" ? V.size.x - 2 : 1) || y != this.INIT_COL_KING[c])
+      return []; //x isn't second rank, or king has moved (shortcut)
+
+    // Castling ?
+    const oppCol = V.GetOppCol(c);
+    let moves = [];
+    let i = 0;
+    // King, then rook:
+    const finalSquares = [
+      [3, 4],
+      [7, 6]
+    ];
+    castlingCheck: for (
+      let castleSide = 0;
+      castleSide < 2;
+      castleSide++ //large, then small
+    ) {
+      if (this.castleFlags[c][castleSide] >= V.size.y) continue;
+      // If this code is reached, rook and king are on initial position
+
+      const rookPos = this.castleFlags[c][castleSide];
+
+      // Nothing on the path of the king ? (and no checks)
+      const castlingPiece = this.getPiece(x, rookPos);
+      const finDist = finalSquares[castleSide][0] - y;
+      let step = finDist / Math.max(1, Math.abs(finDist));
+      i = y;
+      do {
+        if (
+          this.isAttacked([x, i], oppCol) ||
+          (this.board[x][i] != V.EMPTY &&
+            // NOTE: next check is enough, because of chessboard constraints
+            (this.getColor(x, i) != c ||
+              ![V.KING, castlingPiece].includes(this.getPiece(x, i))))
+        ) {
+          continue castlingCheck;
+        }
+        i += step;
+      } while (i != finalSquares[castleSide][0]);
+
+      // Nothing on the path to the rook?
+      step = castleSide == 0 ? -1 : 1;
+      for (i = y + step; i != rookPos; i += step) {
+        if (this.board[x][i] != V.EMPTY) continue castlingCheck;
+      }
+
+      // Nothing on final squares, except maybe king and castling rook?
+      for (i = 0; i < 2; i++) {
+        if (
+          finalSquares[castleSide][i] != rookPos &&
+          this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
+          (
+            this.getPiece(x, finalSquares[castleSide][i]) != V.KING ||
+            this.getColor(x, finalSquares[castleSide][i]) != c
+          )
+        ) {
+          continue castlingCheck;
+        }
+      }
+
+      // If this code is reached, castle is valid
+      moves.push(
+        new Move({
+          appear: [
+            new PiPo({
+              x: x,
+              y: finalSquares[castleSide][0],
+              p: V.KING,
+              c: c
+            }),
+            new PiPo({
+              x: x,
+              y: finalSquares[castleSide][1],
+              p: castlingPiece,
+              c: c
+            })
+          ],
+          vanish: [
+            new PiPo({ x: x, y: y, p: V.KING, c: c }),
+            new PiPo({ x: x, y: rookPos, p: castlingPiece, c: c })
+          ],
+          end:
+            Math.abs(y - rookPos) <= 2
+              ? { x: x, y: rookPos }
+              : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
+        })
+      );
+    }
+
+    return moves;
+  }
+
+  isAttacked(sq, color) {
+    return (
+      super.isAttacked(sq, color) ||
+      this.isAttackedByElephant(sq, color) ||
+      this.isAttackedByCannon(sq, color)
+    );
+  }
+
+  isAttackedByElephant(sq, color) {
+    return (
+      this.isAttackedBySlideNJump(
+        sq, color, V.ELEPHANT, V.steps[V.ELEPHANT], "oneStep"
+      )
+    );
+  }
+
+  isAttackedByCannon([x, y], color) {
+    // Reversed process: is there an obstacle in line,
+    // and a cannon next in the same line?
+    for (const step of V.steps[V.ROOK]) {
+      let [i, j] = [x+step[0], y+step[1]];
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        i += step[0];
+        j += step[1];
+      }
+      if (V.OnBoard(i, j)) {
+        // Keep looking in this direction
+        i += step[0];
+        j += step[1];
+        while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+          i += step[0];
+          j += step[1];
+        }
+        if (
+          V.OnBoard(i, j) &&
+          this.getPiece(i, j) == V.CANNON &&
+          this.getColor(i, j) == color
+        ) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  updateCastleFlags(move, piece) {
+    const c = V.GetOppCol(this.turn);
+    const firstRank = (c == "w" ? V.size.x - 2 : 1);
+    // Update castling flags if rooks are moved
+    const oppCol = this.turn;
+    const oppFirstRank = V.size.x - 1 - firstRank;
+    if (piece == V.KING)
+      this.castleFlags[c] = [V.size.y, V.size.y];
+    else if (
+      move.start.x == firstRank && //our rook moves?
+      this.castleFlags[c].includes(move.start.y)
+    ) {
+      const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
+      this.castleFlags[c][flagIdx] = V.size.y;
+    }
+    // NOTE: not "else if" because a rook could take an opposing rook
+    if (
+      move.end.x == oppFirstRank && //we took opponent rook?
+      this.castleFlags[oppCol].includes(move.end.y)
+    ) {
+      const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
+      this.castleFlags[oppCol][flagIdx] = V.size.y;
+    }
+  }
+
+  static get VALUES() {
+    return Object.assign(
+      { e: 3, c: 5 },
+      ChessRules.VALUES
+    );
+  }
+
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
+  static GenRandInitFen(randomness) {
+    if (randomness == 0) {
+      return (
+        "c8c/ernbqkbnre/pppppppppp/91/91/91/91/PPPPPPPPPP/ERNBQKBNRE/C8C " +
+        "w 0 bibi -"
+      );
+    }
+
+    let pieces = { w: new Array(10), b: new Array(10) };
+    let flags = "";
+    // Shuffle pieces on second (and before-last rank if randomness == 2)
+    for (let c of ["w", "b"]) {
+      if (c == 'b' && randomness == 1) {
+        pieces['b'] = pieces['w'];
+        flags += flags;
+        break;
+      }
+
+      let positions = ArrayFun.range(10);
+
+      // Get random squares for bishops + elephants
+      const be1Pos = sample([0, 2, 4, 6, 8], 2);
+      const be2Pos = sample([1, 3, 5, 7, 9], 2);
+      const bishop1Pos = be1Pos[0];
+      const bishop2Pos = be2Pos[0];
+      const elephant1Pos = be1Pos[1];
+      const elephant2Pos = be2Pos[1];
+      // Remove chosen squares
+      (be1Pos.concat(be2Pos)).sort((x, y) => y - x).forEach(pos => {
+        positions.splice(pos, 1);
+      });
+
+      let randIndex = randInt(6);
+      const knight1Pos = positions[randIndex];
+      positions.splice(randIndex, 1);
+      randIndex = randInt(5);
+      const knight2Pos = positions[randIndex];
+      positions.splice(randIndex, 1);
+
+      randIndex = randInt(4);
+      const queenPos = positions[randIndex];
+      positions.splice(randIndex, 1);
+
+      const rook1Pos = positions[0];
+      const kingPos = positions[1];
+      const rook2Pos = positions[2];
+
+      pieces[c][elephant1Pos] = "e";
+      pieces[c][rook1Pos] = "r";
+      pieces[c][knight1Pos] = "n";
+      pieces[c][bishop1Pos] = "b";
+      pieces[c][queenPos] = "q";
+      pieces[c][kingPos] = "k";
+      pieces[c][bishop2Pos] = "b";
+      pieces[c][knight2Pos] = "n";
+      pieces[c][rook2Pos] = "r";
+      pieces[c][elephant2Pos] = "e";
+      flags += V.CoordToColumn(rook1Pos) + V.CoordToColumn(rook2Pos);
+    }
+    // Add turn + flags + enpassant
+    return (
+      "c8c/" + pieces["b"].join("") +
+      "/pppppppppp/91/91/91/91/PPPPPPPPPP/" +
+      pieces["w"].join("").toUpperCase() + "/C8C" +
+      " w 0 " + flags + " -"
+    );
+  }
+};