Add Tencubed and Omega variants + some fixes (updateCastleFlags()) + cleaner FEN...
[vchess.git] / client / src / variants / Omega.js
diff --git a/client/src/variants/Omega.js b/client/src/variants/Omega.js
new file mode 100644 (file)
index 0000000..fa95f5e
--- /dev/null
@@ -0,0 +1,450 @@
+import { ChessRules, Move, PiPo } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { randInt } from "@/utils/alea";
+
+export class OmegaRules extends ChessRules {
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      {
+        initShift: { w: 2, b: 2 },
+        threeSquares: true,
+        promotions:
+          ChessRules.PawnSpecs.promotions.concat([V.CHAMPION, V.WIZARD])
+      }
+    );
+  }
+
+  // For space between corners:
+  static get NOTHING() {
+    return "xx";
+  }
+
+  static board2fen(b) {
+    if (b[0] == 'x') return 'x';
+    return ChessRules.board2fen(b);
+  }
+
+  static fen2board(f) {
+    if (f == 'x') return V.NOTHING;
+    return ChessRules.fen2board(f);
+  }
+
+  getPpath(b) {
+    if (b[0] == 'x') return "Omega/nothing";
+    return ([V.CHAMPION, V.WIZARD].includes(b[1]) ? "Omega/" : "") + b;
+  }
+
+  // NOTE: keep this extensive check because the board has holes
+  static IsGoodEnpassant(enpassant) {
+    if (enpassant != "-") {
+      const squares = enpassant.split(",");
+      if (squares.length > 2) return false;
+      for (let sq of squares) {
+        const ep = V.SquareToCoords(sq);
+        if (isNaN(ep.x) || !V.OnBoard(ep)) return false;
+      }
+    }
+    return true;
+  }
+
+  static get size() {
+    return { x: 12, y: 12 };
+  }
+
+  static OnBoard(x, y) {
+    return (
+      (x >= 1 && x <= 10 && y >= 1 && y <= 10) ||
+      (x == 11 && [0, 11].includes(y)) ||
+      (x == 0 && [0, 11].includes(y))
+    );
+  }
+
+  // Dabbabah + alfil + wazir
+  static get CHAMPION() {
+    return "c";
+  }
+
+  // Camel + ferz
+  static get WIZARD() {
+    return "w";
+  }
+
+  static get PIECES() {
+    return ChessRules.PIECES.concat([V.CHAMPION, V.WIZARD]);
+  }
+
+  static get steps() {
+    return Object.assign(
+      {},
+      ChessRules.steps,
+      {
+        w: [
+          [-3, -1],
+          [-3, 1],
+          [-1, -3],
+          [-1, 3],
+          [1, -3],
+          [1, 3],
+          [3, -1],
+          [3, 1],
+          [-1, -1],
+          [-1, 1],
+          [1, -1],
+          [1, 1]
+        ],
+        c: [
+          [1, 0],
+          [-1, 0],
+          [0, 1],
+          [0, -1],
+          [2, 2],
+          [2, -2],
+          [-2, 2],
+          [-2, -2],
+          [-2, 0],
+          [0, -2],
+          [2, 0],
+          [0, 2]
+        ]
+      }
+    );
+  }
+
+  static GenRandInitFen(randomness) {
+    if (randomness == 0) {
+      return (
+        "wxxxxxxxxxxw/xcrnbqkbnrcx/xppppppppppx/x91x/x91x/x91x/" +
+        "x91x/x91x/x91x/xPPPPPPPPPPx/xCRNBQKBNRCx/WxxxxxxxxxxW " +
+        "w 0 cjcj -"
+      );
+    }
+
+    let pieces = { w: new Array(10), b: new Array(10) };
+    let flags = "";
+    // Shuffle pieces on first (and 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
+      let randIndex = 2 * randInt(5);
+      const bishop1Pos = positions[randIndex];
+      // The second bishop must be on a square of different color
+      let randIndex_tmp = 2 * randInt(5) + 1;
+      const bishop2Pos = positions[randIndex_tmp];
+      positions.splice(Math.max(randIndex, randIndex_tmp), 1);
+      positions.splice(Math.min(randIndex, randIndex_tmp), 1);
+
+      // Get random squares for champions
+      randIndex = 2 * randInt(4);
+      let bishopSameColorPos = (bishop1Pos % 2 == 0 ? bishop1Pos : bishop2Pos);
+      if (randIndex >= bishopSameColorPos) randIndex += 2;
+      const champion1Pos = positions[randIndex];
+      // The second champion must be on a square of different color
+      randIndex_tmp = 2 * randInt(4) + 1;
+      bishopSameColorPos = (bishop1Pos % 2 == 0 ? bishop1Pos : bishop2Pos);
+      if (randIndex_tmp >= bishopSameColorPos) randIndex_tmp += 2;
+      const champion2Pos = positions[randIndex_tmp];
+      positions.splice(Math.max(randIndex, randIndex_tmp), 1);
+      positions.splice(Math.min(randIndex, randIndex_tmp), 1);
+
+      // Get random squares for other pieces
+      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);
+
+      // Rooks and king positions are now fixed
+      const rook1Pos = positions[0];
+      const kingPos = positions[1];
+      const rook2Pos = positions[2];
+
+      pieces[c][champion1Pos] = "c";
+      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][champion2Pos] = "c";
+      flags += V.CoordToColumn(rook1Pos) + V.CoordToColumn(rook2Pos);
+    }
+    // Add turn + flags + enpassant
+    return (
+      "wxxxxxxxxxxw/" +
+      "x" + pieces["b"].join("") +
+      "x/xppppppppppx/x91x/x91x/x91x/x91x/x91x/x91x/xPPPPPPPPPPx/x" +
+      pieces["w"].join("").toUpperCase() + "x" +
+      "/WxxxxxxxxxxW " +
+      "w 0 " + flags + " -"
+    );
+  }
+
+  // There may be 2 enPassant squares (if pawn jump 3 squares)
+  getEnpassantFen() {
+    const L = this.epSquares.length;
+    if (!this.epSquares[L - 1]) return "-"; //no en-passant
+    let res = "";
+    this.epSquares[L - 1].forEach(sq => {
+      res += V.CoordsToSquare(sq) + ",";
+    });
+    return res.slice(0, -1); //remove last comma
+  }
+
+  // En-passant after 2-sq or 3-sq jumps
+  getEpSquare(moveOrSquare) {
+    if (!moveOrSquare) return undefined;
+    if (typeof moveOrSquare === "string") {
+      const square = moveOrSquare;
+      if (square == "-") return undefined;
+      let res = [];
+      square.split(",").forEach(sq => {
+        res.push(V.SquareToCoords(sq));
+      });
+      return res;
+    }
+    // Argument is a move:
+    const move = moveOrSquare;
+    const [sx, sy, ex] = [move.start.x, move.start.y, move.end.x];
+    if (this.getPiece(sx, sy) == V.PAWN && Math.abs(sx - ex) >= 2) {
+      const step = (ex - sx) / Math.abs(ex - sx);
+      let res = [
+        {
+          x: sx + step,
+          y: sy
+        }
+      ];
+      if (sx + 2 * step != ex) {
+        // 3-squares jump
+        res.push({
+          x: sx + 2 * step,
+          y: sy
+        });
+      }
+      return res;
+    }
+    return undefined; //default
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    switch (this.getPiece(x, y)) {
+      case V.CHAMPION:
+        return this.getPotentialChampionMoves([x, y]);
+      case V.WIZARD:
+        return this.getPotentialWizardMoves([x, y]);
+      default:
+        return super.getPotentialMovesFrom([x, y]);
+    }
+  }
+
+  getEnpassanCaptures([x, y], shiftX) {
+    const Lep = this.epSquares.length;
+    const epSquare = this.epSquares[Lep - 1];
+    let moves = [];
+    if (!!epSquare) {
+      for (let epsq of epSquare) {
+        // TODO: some redundant checks
+        if (epsq.x == x + shiftX && Math.abs(epsq.y - y) == 1) {
+          let enpassantMove = this.getBasicMove([x, y], [epsq.x, epsq.y]);
+          // WARNING: the captured pawn may be diagonally behind us,
+          // if it's a 3-squares jump and we take on 1st passing square
+          const px = this.board[x][epsq.y] != V.EMPTY ? x : x - shiftX;
+          enpassantMove.vanish.push({
+            x: px,
+            y: epsq.y,
+            p: "p",
+            c: this.getColor(px, epsq.y)
+          });
+          moves.push(enpassantMove);
+        }
+      }
+    }
+    return moves;
+  }
+
+  getPotentialChampionMoves(sq) {
+    return this.getSlideNJumpMoves(sq, V.steps[V.CHAMPION], "oneStep");
+  }
+
+  getPotentialWizardMoves(sq) {
+    return this.getSlideNJumpMoves(sq, V.steps[V.WIZARD], "oneStep");
+  }
+
+  getCastleMoves([x, y], castleInCheck) {
+    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 first rank, or king has moved (shortcut)
+
+    // Castling ?
+    const oppCol = V.GetOppCol(c);
+    let moves = [];
+    let i = 0;
+    // King, then rook:
+    const finalSquares = [
+      [4, 5],
+      [8, 7]
+    ];
+    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
+
+      // NOTE: in some variants this is not a rook
+      const rookPos = this.castleFlags[c][castleSide];
+      if (this.board[x][rookPos] == V.EMPTY || this.getColor(x, rookPos) != c)
+        // Rook is not here, or changed color (see Benedict)
+        continue;
+
+      // 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 (
+          (!castleInCheck && 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.isAttackedByChampion(sq, color) ||
+      this.isAttackedByWizard(sq, color)
+    );
+  }
+
+  isAttackedByWizard(sq, color) {
+    return (
+      this.isAttackedBySlideNJump(
+        sq, color, V.WIZARD, V.steps[V.WIZARD], "oneStep")
+    );
+  }
+
+  isAttackedByChampion(sq, color) {
+    return (
+      this.isAttackedBySlideNJump(
+        sq, color, V.CHAMPION, V.steps[V.CHAMPION], "oneStep")
+    );
+  }
+
+  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 SEARCH_DEPTH() {
+    return 2;
+  }
+
+  // Values taken from https://omegachess.com/strategy.htm
+  static get VALUES() {
+    return {
+      p: 1,
+      n: 2,
+      b: 4,
+      r: 6,
+      q: 12,
+      w: 4,
+      c: 4,
+      k: 1000
+    };
+  }
+};