Add Makpong, Hoppelpoppel, and Jangqi (rules unwritten yet)
[vchess.git] / client / src / variants / Xiangqi.js
index 3889b20..b302433 100644 (file)
@@ -2,6 +2,8 @@ import { ChessRules } from "@/base_rules";
 
 export class XiangqiRules extends ChessRules {
 
+  // NOTE (TODO?) scanKings() could be more efficient (in Jangqi too)
+
   static get Monochrome() {
     return true;
   }
@@ -36,6 +38,10 @@ export class XiangqiRules extends ChessRules {
     return false;
   }
 
+  static get LoseOnRepetition() {
+    return true;
+  }
+
   static get ELEPHANT() {
     return "e";
   }
@@ -61,16 +67,80 @@ export class XiangqiRules extends ChessRules {
   }
 
   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);
+    let moves = [];
+    const piece = this.getPiece(sq[0], sq[1]);
+    switch (piece) {
+      case V.PAWN:
+        moves = this.getPotentialPawnMoves(sq);
+        break;
+      case V.ROOK:
+        moves = super.getPotentialRookMoves(sq);
+        break;
+      case V.KNIGHT:
+        moves = this.getPotentialKnightMoves(sq);
+        break;
+      case V.ELEPHANT:
+        moves = this.getPotentialElephantMoves(sq);
+        break;
+      case V.ADVISOR:
+        moves = this.getPotentialAdvisorMoves(sq);
+        break;
+      case V.KING:
+        moves = this.getPotentialKingMoves(sq);
+        break;
+      case V.CANNON:
+        moves = this.getPotentialCannonMoves(sq);
+        break;
     }
-    return []; //never reached
+    if (piece != V.KING && this.kingPos['w'][1] != this.kingPos['b'][1])
+      return moves;
+    if (this.kingPos['w'][1] == this.kingPos['b'][1]) {
+      const colKing = this.kingPos['w'][1];
+      let intercept = 0; //count intercepting pieces
+      for (let i = this.kingPos['b'][0] + 1; i < this.kingPos['w'][0]; i++) {
+        if (this.board[i][colKing] != V.EMPTY) intercept++;
+      }
+      if (intercept >= 2) return moves;
+      // intercept == 1 (0 is impossible):
+      // Any move not removing intercept is OK
+      return moves.filter(m => {
+        return (
+          // From another column?
+          m.start.y != colKing ||
+          // From behind a king? (including kings themselves!)
+          m.start.x <= this.kingPos['b'][0] ||
+          m.start.x >= this.kingPos['w'][0] ||
+          // Intercept piece moving: must remain in-between
+          (
+            m.end.y == colKing &&
+            m.end.x > this.kingPos['b'][0] &&
+            m.end.x < this.kingPos['w'][0]
+          )
+        );
+      });
+    }
+    // piece == king: check only if move.end.y == enemy king column
+    const color = this.getColor(sq[0], sq[1]);
+    const oppCol = V.GetOppCol(color);
+    // colCheck == -1 if unchecked, 1 if checked and occupied,
+    //              0 if checked and clear
+    let colCheck = -1;
+    return moves.filter(m => {
+      if (m.end.y != this.kingPos[oppCol][1]) return true;
+      if (colCheck < 0) {
+        // Do the check:
+        colCheck = 0;
+        for (let i = this.kingPos['b'][0] + 1; i < this.kingPos['w'][0]; i++) {
+          if (this.board[i][m.end.y] != V.EMPTY) {
+            colCheck++;
+            break;
+          }
+        }
+        return colCheck == 1;
+      }
+      // Check already done:
+      return colCheck == 1;
+    });
   }
 
   getPotentialPawnMoves([x, y]) {
@@ -121,34 +191,55 @@ export class XiangqiRules extends ChessRules {
     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);
+    if (
+      y != 4 ||
+      (c == 'w' && x != V.size.x - 2) ||
+      (c == 'b' && x != 1)
+    ) {
+      // In a corner: only one step available
+      let step = null;
+      const direction = (c == 'w' ? -1 : 1);
+      if ((c == 'w' && x == V.size.x - 1) || (c == 'b' && x == 0)) {
+        // On first line
+        if (y == 3) step = [direction, 1];
+        else step = [direction, -1];
+      }
+      else {
+        // On third line
+        if (y == 3) step = [-direction, 1];
+        else step = [-direction, -1];
+      }
+      return super.getSlideNJumpMoves([x, y], [step], "oneStep");
     }
-    return super.getSlideNJumpMoves([x, y], steps, "oneStep");
+    // In the middle of the palace:
+    return (
+      super.getSlideNJumpMoves([x, y], ChessRules.steps[V.BISHOP], "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);
+    if (
+      y != 4 ||
+      (c == 'w' && x != V.size.x - 2) ||
+      (c == 'b' && x != 1)
+    ) {
+      // On the edge: only two steps available
+      let steps = [];
+      if (x < (c == 'w' ? V.size.x - 1 : 2)) steps.push([1, 0]);
+      if (x > (c == 'w' ? V.size.x - 3 : 0)) steps.push([-1, 0]);
+      if (y > 3) steps.push([0, -1]);
+      if (y < 5) steps.push([0, 1]);
+      return super.getSlideNJumpMoves([x, y], steps, "oneStep");
     }
-    return super.getSlideNJumpMoves([x, y], steps, "oneStep");
+    // In the middle of the palace:
+    return (
+      super.getSlideNJumpMoves([x, y], ChessRules.steps[V.ROOK], "oneStep")
+    );
   }
 
   // NOTE: duplicated from Shako (TODO?)
@@ -191,17 +282,8 @@ export class XiangqiRules extends ChessRules {
   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;
+    return super.isAttackedBySlideNJump(
+      [x, y], color, V.PAWN, [[shiftX, 0], [0, 1], [0, -1]], "oneStep");
   }
 
   knightStepsFromBishopStep(step) {
@@ -255,6 +337,14 @@ export class XiangqiRules extends ChessRules {
     return false;
   }
 
+  getCurrentScore() {
+    if (this.atLeastOneMove()) return "*";
+    // Game over
+    const color = this.turn;
+    // No valid move: I lose!
+    return (color == "w" ? "0-1" : "1-0");
+  }
+
   static get VALUES() {
     return {
       p: 1,
@@ -293,9 +383,20 @@ export class XiangqiRules extends ChessRules {
     return evaluation;
   }
 
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
   static GenRandInitFen() {
     // No randomization here (TODO?)
     return "rneakaenr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNEAKAENR w 0";
   }
 
+  getNotation(move) {
+    let notation = super.getNotation(move);
+    if (move.vanish.length == 2 && move.vanish[0].p == V.PAWN)
+      notation = "P" + substr(notation, 1);
+    return notation;
+  }
+
 };