Add Xiangqi
[vchess.git] / client / src / variants / Xiangqi.js
diff --git a/client/src/variants/Xiangqi.js b/client/src/variants/Xiangqi.js
new file mode 100644 (file)
index 0000000..3889b20
--- /dev/null
@@ -0,0 +1,301 @@
+import { ChessRules } from "@/base_rules";
+
+export class XiangqiRules extends ChessRules {
+
+  static get Monochrome() {
+    return true;
+  }
+
+  static get Notoodark() {
+    return true;
+  }
+
+  static get Lines() {
+    let lines = [];
+    // Draw all inter-squares lines, shifted:
+    for (let i = 0; i < V.size.x; i++)
+      lines.push([[i+0.5, 0.5], [i+0.5, V.size.y-0.5]]);
+    for (let j = 0; j < V.size.y; j++)
+      lines.push([[0.5, j+0.5], [V.size.x-0.5, j+0.5]]);
+    // Add palaces:
+    lines.push([[0.5, 3.5], [2.5, 5.5]]);
+    lines.push([[0.5, 5.5], [2.5, 3.5]]);
+    lines.push([[9.5, 3.5], [7.5, 5.5]]);
+    lines.push([[9.5, 5.5], [7.5, 3.5]]);
+    // Show river:
+    lines.push([[4.5, 0.5], [5.5, 8.5]]);
+    lines.push([[5.5, 0.5], [4.5, 8.5]]);
+    return lines;
+  }
+
+  static get HasFlags() {
+    return false;
+  }
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  static get ELEPHANT() {
+    return "e";
+  }
+
+  static get CANNON() {
+    return "c";
+  }
+
+  static get ADVISOR() {
+    return "a";
+  }
+
+  static get PIECES() {
+    return [V.PAWN, V.ROOK, V.KNIGHT, V.ELEPHANT, V.ADVISOR, V.KING, V.CANNON];
+  }
+
+  getPpath(b) {
+    return "Xiangqi/" + b;
+  }
+
+  static get size() {
+    return { x: 10, y: 9};
+  }
+
+  getPotentialMovesFrom(sq) {
+    switch (this.getPiece(sq[0], sq[1])) {
+      case V.PAWN: return this.getPotentialPawnMoves(sq);
+      case V.ROOK: return super.getPotentialRookMoves(sq);
+      case V.KNIGHT: return this.getPotentialKnightMoves(sq);
+      case V.ELEPHANT: return this.getPotentialElephantMoves(sq);
+      case V.ADVISOR: return this.getPotentialAdvisorMoves(sq);
+      case V.KING: return this.getPotentialKingMoves(sq);
+      case V.CANNON: return this.getPotentialCannonMoves(sq);
+    }
+    return []; //never reached
+  }
+
+  getPotentialPawnMoves([x, y]) {
+    const c = this.getColor(x, y);
+    const shiftX = (c == 'w' ? -1 : 1);
+    const crossedRiver = (c == 'w' && x <= 4 || c == 'b' && x >= 5);
+    const lastRank = (c == 'w' && x == 0 || c == 'b' && x == 9);
+    let steps = [];
+    if (!lastRank) steps.push([shiftX, 0]);
+    if (crossedRiver) {
+      if (y > 0) steps.push([0, -1]);
+      if (y < 9) steps.push([0, 1]);
+    }
+    return super.getSlideNJumpMoves([x, y], steps, "oneStep");
+  }
+
+  knightStepsFromRookStep(step) {
+    if (step[0] == 0) return [ [1, 2*step[1]], [-1, 2*step[1]] ];
+    return [ [2*step[0], 1], [2*step[0], -1] ];
+  }
+
+  getPotentialKnightMoves([x, y]) {
+    let steps = [];
+    for (let rookStep of ChessRules.steps[V.ROOK]) {
+      const [i, j] = [x + rookStep[0], y + rookStep[1]];
+      if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        Array.prototype.push.apply(steps,
+          // These moves might be impossible, but need to be checked:
+          this.knightStepsFromRookStep(rookStep));
+      }
+    }
+    return super.getSlideNJumpMoves([x, y], steps, "oneStep");
+  }
+
+  getPotentialElephantMoves([x, y]) {
+    let steps = [];
+    const c = this.getColor(x, y);
+    for (let bishopStep of ChessRules.steps[V.BISHOP]) {
+      const [i, j] = [x + bishopStep[0], y + bishopStep[1]];
+      if (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        const [newX, newY] = [x + 2*bishopStep[0], y + 2*bishopStep[1]];
+        if ((c == 'w' && newX >= 5) || (c == 'b' && newX <= 4))
+          // A priori valid (elephant don't cross the river)
+          steps.push(bishopStep.map(s => 2*s));
+          // "out of board" checks delayed to next method
+      }
+    }
+    return super.getSlideNJumpMoves([x, y], steps, "oneStep");
+  }
+
+  insidePalace(x, y, c) {
+    return (
+      (y >= 3 && y <= 5) &&
+      (
+        (c == 'w' && x >= 7) ||
+        (c == 'b' && x <= 2)
+      )
+    );
+  }
+
+  getPotentialAdvisorMoves([x, y]) {
+    // Diagonal steps inside palace
+    let steps = [];
+    const c = this.getColor(x, y);
+    for (let s of ChessRules.steps[V.BISHOP]) {
+      if (this.insidePalace(x + s[0], y + s[1], c)) steps.push(s);
+    }
+    return super.getSlideNJumpMoves([x, y], steps, "oneStep");
+  }
+
+  getPotentialKingMoves([x, y]) {
+    // Orthogonal steps inside palace
+    let steps = [];
+    const c = this.getColor(x, y);
+    for (let s of ChessRules.steps[V.ROOK]) {
+      if (this.insidePalace(x + s[0], y + s[1], c)) steps.push(s);
+    }
+    return super.getSlideNJumpMoves([x, y], steps, "oneStep");
+  }
+
+  // NOTE: duplicated from Shako (TODO?)
+  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;
+  }
+
+  // (King) Never attacked by advisor, since it stays in the palace
+  // Also, never attacked by elephants since they don't cross the river.
+  isAttacked(sq, color) {
+    return (
+      this.isAttackedByPawn(sq, color) ||
+      super.isAttackedByRook(sq, color) ||
+      this.isAttackedByKnight(sq, color) ||
+      this.isAttackedByCannon(sq, color)
+    );
+  }
+
+  isAttackedByPawn([x, y], color) {
+    // The pawn necessarily crossed the river (attack on king)
+    const shiftX = (color == 'w' ? 1 : -1); //shift from king
+    for (let s of [[shiftX, 0], [0, 1], [0, -1]]) {
+      const [i, j] = [x + s[0], y + s[1]];
+      if (
+        this.board[i][j] != V.EMPTY &&
+        this.getColor(i, j) == color &&
+        this.getPiece(i, j) == V.PAWN
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  knightStepsFromBishopStep(step) {
+    return [ [2*step[0], step[1]], [step[0], 2*step[1]] ];
+  }
+
+  isAttackedByKnight([x, y], color) {
+    // Check bishop steps: if empty, look continuation knight step
+    let steps = [];
+    for (let s of ChessRules.steps[V.BISHOP]) {
+      const [i, j] = [x + s[0], y + s[1]];
+      if (
+        V.OnBoard(i, j) &&
+        this.board[i][j] == V.EMPTY
+      ) {
+        Array.prototype.push.apply(steps, this.knightStepsFromBishopStep(s));
+      }
+    }
+    return (
+      super.isAttackedBySlideNJump([x, y], color, V.KNIGHT, steps, "oneStep")
+    );
+  }
+
+  // NOTE: duplicated from Shako (TODO?)
+  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;
+  }
+
+  static get VALUES() {
+    return {
+      p: 1,
+      r: 9,
+      n: 4,
+      e: 2.5,
+      a: 2,
+      c: 4.5,
+      k: 1000
+    };
+  }
+
+  evalPosition() {
+    let evaluation = 0;
+    for (let i = 0; i < V.size.x; i++) {
+      for (let j = 0; j < V.size.y; j++) {
+        if (this.board[i][j] != V.EMPTY) {
+          const c = this.getColor(i, j);
+          const sign = (c == 'w' ? 1 : -1);
+          const piece = this.getPiece(i, j);
+          let pieceEval = V.VALUES[this.getPiece(i, j)];
+          if (
+            piece == V.PAWN &&
+            (
+              (c == 'w' && i <= 4) ||
+              (c == 'b' && i >= 5)
+            )
+          ) {
+            // Pawn crossed the river: higher value
+            pieceEval++;
+          }
+          evaluation += sign * pieceEval;
+        }
+      }
+    }
+    return evaluation;
+  }
+
+  static GenRandInitFen() {
+    // No randomization here (TODO?)
+    return "rneakaenr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNEAKAENR w 0";
+  }
+
+};