Add Tencubed and Omega variants + some fixes (updateCastleFlags()) + cleaner FEN...
[vchess.git] / client / src / variants / Cannibal.js
index fc0ef0b..f7a9514 100644 (file)
@@ -1,6 +1,88 @@
-import { ChessRules } from "@/base_rules";
+import { ChessRules, Move, PiPo } from "@/base_rules";
 
 export class CannibalRules extends ChessRules {
+  static get KING_CODE() {
+    return {
+      'p': 's',
+      'r': 'u',
+      'n': 'o',
+      'b': 'c',
+      'q': 't'
+    };
+  }
+
+  static get KING_DECODE() {
+    return {
+      's': 'p',
+      'u': 'r',
+      'o': 'n',
+      'c': 'b',
+      't': 'q'
+    };
+  }
+
+  // Kings may be disguised:
+  getPiece(x, y) {
+    const piece = this.board[x][y].charAt(1);
+    if (Object.keys(V.KING_DECODE).includes(piece))
+      return V.KING_DECODE[piece];
+    return piece;
+  }
+
+  getPpath(b) {
+    return (Object.keys(V.KING_DECODE).includes(b[1]) ? "Cannibal/" : "") + b;
+  }
+
+  static IsGoodPosition(position) {
+    if (position.length == 0) return false;
+    const rows = position.split("/");
+    if (rows.length != V.size.x) return false;
+    let kings = { "w": 0, "b": 0 };
+    const allPiecesCodes = V.PIECES.concat(Object.keys(V.KING_DECODE));
+    const kingBlackCodes = Object.keys(V.KING_DECODE).concat(['k']);
+    const kingWhiteCodes =
+      Object.keys(V.KING_DECODE).map(k => k.toUpperCase()).concat(['K']);
+    for (let row of rows) {
+      let sumElts = 0;
+      for (let i = 0; i < row.length; i++) {
+        if (kingBlackCodes.includes(row[i])) kings['b']++;
+        else if (kingWhiteCodes.includes(row[i])) kings['w']++;
+        if (allPiecesCodes.includes(row[i].toLowerCase())) sumElts++;
+        else {
+          const num = parseInt(row[i]);
+          if (isNaN(num)) return false;
+          sumElts += num;
+        }
+      }
+      if (sumElts != V.size.y) return false;
+    }
+    // Both kings should be on board, only one of each color:
+    if (Object.values(kings).some(v => v != 1)) return false;
+    return true;
+  }
+
+  // Kings may be disguised:
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    const rows = V.ParseFen(fen).position.split("/");
+    if (this.kingPos["w"][0] < 0 || this.kingPos["b"][0] < 0) {
+      for (let i = 0; i < rows.length; i++) {
+        let k = 0; //column index on board
+        for (let j = 0; j < rows[i].length; j++) {
+          const piece = rows[i].charAt(j);
+          if (Object.keys(V.KING_DECODE).includes(piece.toLowerCase())) {
+            const color = (piece.charCodeAt(0) <= 90 ? 'w' : 'b');
+            this.kingPos[color] = [i, k];
+          } else {
+            const num = parseInt(rows[i].charAt(j));
+            if (!isNaN(num)) k += num - 1;
+          }
+          k++;
+        }
+      }
+    }
+  }
+
   // Trim all non-capturing moves
   static KeepCaptures(moves) {
     return moves.filter(m => m.vanish.length == 2 && m.appear.length == 1);
@@ -26,20 +108,80 @@ export class CannibalRules extends ChessRules {
     return false;
   }
 
-  getPotentialMovesFrom([x, y]) {
-    let moves = super.getPotentialMovesFrom([x, y]);
-    // Transform capturers, except for the king
-    moves.forEach(m => {
-      if (
-        m.appear[0].p != V.KING &&
-        m.vanish.length == 2 &&
-        m.appear.length == 1 &&
-        m.vanish[0].p != m.vanish[1].p
-      ) {
-        m.appear[0].p = m.vanish[1].p;
-      }
+  // Because of the disguised kings, getPiece() could be wrong:
+  // use board[x][y][1] instead (always valid).
+  getBasicMove([sx, sy], [ex, ey], tr) {
+    const initColor = this.getColor(sx, sy);
+    const initPiece = this.board[sx][sy].charAt(1);
+    let mv = new Move({
+      appear: [
+        new PiPo({
+          x: ex,
+          y: ey,
+          c: tr ? tr.c : initColor,
+          p: tr ? tr.p : initPiece
+        })
+      ],
+      vanish: [
+        new PiPo({
+          x: sx,
+          y: sy,
+          c: initColor,
+          p: initPiece
+        })
+      ]
     });
-    return moves;
+
+    // The opponent piece disappears if we take it
+    if (this.board[ex][ey] != V.EMPTY) {
+      mv.vanish.push(
+        new PiPo({
+          x: ex,
+          y: ey,
+          c: this.getColor(ex, ey),
+          p: this.board[ex][ey].charAt(1)
+        })
+      );
+
+      // If the captured piece has a different nature: take it as well
+      if (mv.vanish[0].p != mv.vanish[1].p) {
+        if (
+          mv.vanish[0].p == V.KING ||
+          Object.keys(V.KING_DECODE).includes(mv.vanish[0].p)
+        ) {
+          mv.appear[0].p = V.KING_CODE[mv.vanish[1].p];
+        } else mv.appear[0].p = mv.vanish[1].p;
+      }
+    }
+    else if (!!tr && mv.vanish[0].p != V.PAWN)
+      // Special case of a non-capturing king-as-pawn promotion
+      mv.appear[0].p = V.KING_CODE[tr.p];
+
+    return mv;
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    const piece = this.board[x][y].charAt(1);
+    if (Object.keys(V.KING_DECODE).includes(piece))
+      return super.getPotentialMovesFrom([x, y], V.KING_DECODE[piece]);
+    return super.getPotentialMovesFrom([x, y], piece);
+  }
+
+  addPawnMoves([x1, y1], [x2, y2], moves) {
+    let finalPieces = [V.PAWN];
+    const color = this.turn;
+    const lastRank = (color == "w" ? 0 : V.size.x - 1);
+    if (x2 == lastRank) {
+      if (this.board[x2][y2] != V.EMPTY)
+        // Cannibal rules: no choice if capture
+        finalPieces = [this.getPiece(x2, y2)];
+      else finalPieces = V.PawnSpecs.promotions;
+    }
+    let tr = null;
+    for (let piece of finalPieces) {
+      tr = (piece != V.PAWN ? { c: color, p: piece } : null);
+      moves.push(this.getBasicMove([x1, y1], [x2, y2], tr));
+    }
   }
 
   getPossibleMovesFrom(sq) {
@@ -52,11 +194,56 @@ export class CannibalRules extends ChessRules {
 
   getAllValidMoves() {
     const moves = super.getAllValidMoves();
-    if (moves.some(m => m.vanish.length == 2)) return V.KeepCaptures(moves);
+    if (moves.some(m => m.vanish.length == 2 && m.appear.length == 1))
+      return V.KeepCaptures(moves);
     return moves;
   }
 
-  static get SEARCH_DEPTH() {
-    return 4;
+  postPlay(move) {
+    const c = V.GetOppCol(this.turn);
+    const piece = move.appear[0].p;
+    // Update king position + flags
+    if (piece == V.KING || Object.keys(V.KING_DECODE).includes(piece)) {
+      this.kingPos[c][0] = move.appear[0].x;
+      this.kingPos[c][1] = move.appear[0].y;
+      this.castleFlags[c] = [V.size.y, V.size.y];
+    }
+    // Next call is still required because the king may eat an opponent's rook
+    // TODO: castleFlags will be turned off twice then.
+    super.updateCastleFlags(move, piece);
+  }
+
+  postUndo(move) {
+    // (Potentially) Reset king position
+    const c = this.getColor(move.start.x, move.start.y);
+    const piece = move.appear[0].p;
+    if (piece == V.KING || Object.keys(V.KING_DECODE).includes(piece))
+      this.kingPos[c] = [move.start.x, move.start.y];
+  }
+
+  static get VALUES() {
+    return {
+      p: 1,
+      r: 5,
+      n: 3,
+      b: 3,
+      q: 9,
+      k: 5
+    };
+  }
+
+  getNotation(move) {
+    let notation = super.getNotation(move);
+    const lastRank = (move.appear[0].c == "w" ? 0 : 7);
+    if (
+      move.end.x != lastRank &&
+      this.getPiece(move.start.x, move.start.y) == V.PAWN &&
+      move.vanish.length == 2 &&
+      move.appear[0].p != V.PAWN
+    ) {
+      // Fix "promotion" (transform indicator) from base_rules notation
+      notation = notation.slice(0, -2);
+    }
+    return notation;
   }
 };