Add Fugue variant
[vchess.git] / client / src / variants / Fugue.js
diff --git a/client/src/variants/Fugue.js b/client/src/variants/Fugue.js
new file mode 100644 (file)
index 0000000..12002fa
--- /dev/null
@@ -0,0 +1,584 @@
+import { ChessRules, PiPo, Move } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { shuffle } from "@/utils/alea";
+
+export class FugueRules extends ChessRules {
+
+  static get HasFlags() {
+    return false;
+  }
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  static get LoseOnRepetition() {
+    return true;
+  }
+
+  static get IMMOBILIZER() {
+    return 'i';
+  }
+  static get PUSHME_PULLYOU() {
+    return 'u';
+  }
+  static get ARCHER() {
+    return 'a';
+  }
+  static get SHIELD() {
+    return 's';
+  }
+  static get LONG_LEAPER() {
+    return 'l';
+  }
+  static get SWAPPER() {
+    return 'w';
+  }
+
+  static get PIECES() {
+    return [
+      V.QUEEN,
+      V.KING,
+      V.IMMOBILIZER,
+      V.PUSHME_PULLYOU,
+      V.ARCHER,
+      V.SHIELD,
+      V.LONG_LEAPER,
+      V.SWAPPER
+    ];
+  }
+
+  getPpath(b) {
+    if (['p', 'q', 'k'].includes(b[1])) return b;
+    return "Fugue/" + b;
+  }
+
+  getPPpath(m) {
+    // The only "choice" case is between a swap and a mutual destruction:
+    // show empty square in case of mutual destruction.
+    if (m.appear.length == 0) return "Rococo/empty";
+    return this.getPpath(m.appear[0].c + m.appear[0].p);
+  }
+
+  scanKings(fen) {
+    // No castling, but keep track of kings for faster game end checks
+    this.kingPos = { w: [-1, -1], b: [-1, -1] };
+    const fenParts = fen.split(" ");
+    const position = fenParts[0].split("/");
+    for (let i = 0; i < position.length; i++) {
+      let k = 0;
+      for (let j = 0; j < position[i].length; j++) {
+        switch (position[i].charAt(j)) {
+          case "k":
+            this.kingPos["b"] = [i, k];
+            break;
+          case "K":
+            this.kingPos["w"] = [i, k];
+            break;
+          default: {
+            const num = parseInt(position[i].charAt(j), 10);
+            if (!isNaN(num)) k += num - 1;
+          }
+        }
+        k++;
+      }
+    }
+  }
+
+  // Is piece on square (x,y) immobilized?
+  isImmobilized([x, y]) {
+    const piece = this.getPiece(x, y);
+    if (piece == V.IMMOBILIZER) return false;
+    const oppCol = V.GetOppCol(this.getColor(x, y));
+    const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    for (let step of adjacentSteps) {
+      const [i, j] = [x + step[0], y + step[1]];
+      if (
+        V.OnBoard(i, j) &&
+        this.board[i][j] != V.EMPTY &&
+        this.getColor(i, j) == oppCol
+      ) {
+        if (this.getPiece(i, j) == V.IMMOBILIZER) return true;
+      }
+    }
+    return false;
+  }
+
+  isProtected([x, y]) {
+    const color = this.getColor(x, y);
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    for (let s of steps) {
+      const [i, j] = [x + s[0], y + s[1]];
+      if (
+        V.OnBoard(i, j) &&
+        this.getColor(i, j) == color &&
+        this.getPiece(i, j) == V.SHIELD
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  canTake([x1, y1], [x2, y2]) {
+    return !this.isProtected([x2, y2]) && super.canTake([x1, y1], [x2, y2]);
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    // Pre-check: is thing on this square immobilized?
+    if (this.isImmobilized([x, y])) return [];
+    const piece = this.getPiece(x, y);
+    let moves = [];
+    switch (piece) {
+      case V.PAWN: return this.getPotentialPawnMoves([x, y]);
+      case V.IMMOBILIZER: return this.getPotentialImmobilizerMoves([x, y]);
+      case V.PUSHME_PULLYOU: return this.getPotentialPushmePullyuMoves([x, y]);
+      case V.ARCHER: return this.getPotentialArcherMoves([x, y]);
+      case V.SHIELD: return this.getPotentialShieldMoves([x, y]);
+      case V.KING: return this.getPotentialKingMoves([x, y]);
+      case V.QUEEN: return super.getPotentialQueenMoves([x, y]);
+      case V.LONG_LEAPER: return this.getPotentialLongLeaperMoves([x, y]);
+      case V.SWAPPER: return this.getPotentialSwapperMoves([x, y]);
+    }
+  }
+
+  getSlideNJumpMoves([x, y], steps, oneStep) {
+    const piece = this.getPiece(x, y);
+    let moves = [];
+    outerLoop: for (let step of steps) {
+      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]));
+        if (oneStep !== undefined) continue outerLoop;
+        i += step[0];
+        j += step[1];
+      }
+      // Only queen and king can take on occupied square:
+      if (
+        [V.KING, V.QUEEN].includes(piece) &&
+        V.OnBoard(i, j) &&
+        this.canTake([x, y], [i, j])
+      ) {
+        moves.push(this.getBasicMove([x, y], [i, j]));
+      }
+    }
+    return moves;
+  }
+
+  // "Cannon/grasshopper pawn"
+  getPotentialPawnMoves([x, y]) {
+    const c = this.turn;
+    const oppCol = V.GetOppCol(c);
+    const lastRank = (c == 'w' ? 0 : 7);
+    let canResurect = {
+      [V.QUEEN]: true,
+      [V.IMMOBILIZER]: true,
+      [V.PUSHME_PULLYOU]: true,
+      [V.ARCHER]: true,
+      [V.SHIELD]: true,
+      [V.LONG_LEAPER]: true,
+      [V.SWAPPER]: true
+    };
+    for (let i = 0; i < 8; i++) {
+      for (let j = 0; j < 8; j++) {
+        if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == c) {
+          const pIJ = this.getPiece(i, j);
+          if (![V.PAWN, V.KING].includes(pIJ)) canResurect[pIJ] = false;
+        }
+      }
+    }
+    let moves = [];
+    const addPromotions = sq => {
+      // Optional promotion
+      Object.keys(canResurect).forEach(p => {
+        if (canResurect[p]) {
+          moves.push(
+            this.getBasicMove([x, y], [sq[0], sq[1]], { c: c, p: p }));
+        }
+      });
+    }
+    const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    adjacentSteps.forEach(step => {
+      const [i, j] = [x + step[0], y + step[1]];
+      if (V.OnBoard(i, j)) {
+        if (this.board[i][j] == V.EMPTY) {
+          moves.push(this.getBasicMove([x, y], [i, j]));
+          if (i == lastRank) addPromotions([i, j]);
+        }
+        else {
+          // Try to leap over:
+          const [ii, jj] = [i + step[0], j + step[1]];
+          if (
+            V.OnBoard(ii, jj) &&
+            (
+              this.board[ii][jj] == V.EMPTY ||
+              this.getColor(ii, jj) == oppCol && !this.isProtected([ii, jj])
+            )
+          ) {
+            moves.push(this.getBasicMove([x, y], [ii, jj]));
+            if (ii == lastRank) addPromotions([ii, jj]);
+          }
+        }
+      }
+    });
+    return moves;
+  }
+
+  getPotentialKingMoves(sq) {
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    return this.getSlideNJumpMoves(sq, steps, "oneStep");
+  }
+
+  // NOTE: not really captures, but let's keep the name
+  getSwapperCaptures([x, y]) {
+    let moves = [];
+    const oppCol = V.GetOppCol(this.turn);
+    // Simple: if something is visible, we can swap
+    V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(step => {
+      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) && this.getColor(i, j) == oppCol) {
+        const oppPiece = this.getPiece(i, j);
+        let m = this.getBasicMove([x, y], [i, j]);
+        m.appear.push(
+          new PiPo({
+            x: x,
+            y: y,
+            c: oppCol,
+            p: this.getPiece(i, j)
+          })
+        );
+        moves.push(m);
+        if (
+          i == x + step[0] && j == y + step[1] &&
+          !this.isProtected([i, j])
+        ) {
+          // Add mutual destruction option:
+          m = new Move({
+            start: { x: x, y: y},
+            end: { x: i, y: j },
+            appear: [],
+            // TODO: is copying necessary here?
+            vanish: JSON.parse(JSON.stringify(m.vanish))
+          });
+          moves.push(m);
+        }
+      }
+    });
+    return moves;
+  }
+
+  getPotentialSwapperMoves(sq) {
+    return (
+      super.getPotentialQueenMoves(sq).concat(this.getSwapperCaptures(sq))
+    );
+  }
+
+  getLongLeaperCaptures([x, y]) {
+    // Look in every direction for captures
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    const color = this.turn;
+    const oppCol = V.GetOppCol(color);
+    let moves = [];
+    const piece = this.getPiece(x, y);
+    outerLoop: for (let step of steps) {
+      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) ||
+        this.getColor(i, j) == color ||
+        this.isProtected([i, j])
+      ) {
+        continue;
+      }
+      let [ii, jj] = [i + step[0], j + step[1]];
+      const vanished = [
+        new PiPo({ x: x, y: y, c: color, p: piece }),
+        new PiPo({ x: i, y: j, c: oppCol, p: this.getPiece(i, j)})
+      ];
+      while (V.OnBoard(ii, jj) && this.board[ii][jj] == V.EMPTY) {
+        moves.push(
+          new Move({
+            appear: [new PiPo({ x: ii, y: jj, c: color, p: piece })],
+            vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required?
+            start: { x: x, y: y },
+            end: { x: ii, y: jj }
+          })
+        );
+        ii += step[0];
+        jj += step[1];
+      }
+    }
+    return moves;
+  }
+
+  getPotentialLongLeaperMoves(sq) {
+    return (
+      super.getPotentialQueenMoves(sq).concat(this.getLongLeaperCaptures(sq))
+    );
+  }
+
+  completeAndFilterPPcaptures(moves) {
+    if (moves.length == 0) return [];
+    const [x, y] = [moves[0].start.x, moves[0].start.y];
+    const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    let capturingDirStart = {};
+    const oppCol = V.GetOppCol(this.turn);
+    // Useful precomputation:
+    adjacentSteps.forEach(step => {
+      const [i, j] = [x + step[0], y + step[1]];
+      if (
+        V.OnBoard(i, j) &&
+        this.board[i][j] != V.EMPTY &&
+        this.getColor(i, j) == oppCol
+      ) {
+        capturingDirStart[step[0] + "_" + step[1]] = {
+          p: this.getPiece(i, j),
+          canTake: !this.isProtected([i, j])
+        };
+      }
+    });
+    moves.forEach(m => {
+      const step = [
+        m.end.x != x ? (m.end.x - x) / Math.abs(m.end.x - x) : 0,
+        m.end.y != y ? (m.end.y - y) / Math.abs(m.end.y - y) : 0
+      ];
+      // TODO: this test should be done only once per direction
+      const capture = capturingDirStart[(-step[0]) + "_" + (-step[1])];
+      if (!!capture && capture.canTake) {
+        const [i, j] = [x - step[0], y - step[1]];
+        m.vanish.push(
+          new PiPo({
+            x: i,
+            y: j,
+            p: capture.p,
+            c: oppCol
+          })
+        );
+      }
+      // Also test the end (advancer effect)
+      const [i, j] = [m.end.x + step[0], m.end.y + step[1]];
+      if (
+        V.OnBoard(i, j) &&
+        this.board[i][j] != V.EMPTY &&
+        this.getColor(i, j) == oppCol &&
+        !this.isProtected([i, j])
+      ) {
+        m.vanish.push(
+          new PiPo({
+            x: i,
+            y: j,
+            p: this.getPiece(i, j),
+            c: oppCol
+          })
+        );
+      }
+    });
+    // Forbid "double captures"
+    return moves.filter(m => m.vanish.length <= 2);
+  }
+
+  getPotentialPushmePullyuMoves(sq) {
+    let moves = super.getPotentialQueenMoves(sq);
+    return this.completeAndFilterPPcaptures(moves);
+  }
+
+  getPotentialImmobilizerMoves(sq) {
+    // Immobilizer doesn't capture
+    return super.getPotentialQueenMoves(sq);
+  }
+
+  getPotentialArcherMoves([x, y]) {
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    let moves = this.getSlideNJumpMoves([x, y], steps);
+    const c = this.turn;
+    const oppCol = V.GetOppCol(c);
+    // Add captures
+    for (let s of steps) {
+      let [i, j] = [x + s[0], y + s[1]];
+      let stepCounter = 1;
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        i += s[0];
+        j += s[1];
+        stepCounter++;
+      }
+      if (
+        V.OnBoard(i, j) &&
+        this.getColor(i, j) == oppCol &&
+        !this.isProtected([i, j])
+      ) {
+        let shootOk = (stepCounter <= 2);
+        if (!shootOk) {
+          // try to find a spotting piece:
+          for (let ss of steps) {
+            let [ii, jj] = [i + ss[0], j + ss[1]];
+            if (V.OnBoard(ii, jj)) {
+              if (this.board[ii][jj] != V.EMPTY) {
+                if (this.getColor(ii, jj) == c) {
+                  shootOk = true;
+                  break;
+                }
+              }
+              else {
+                ii += ss[0];
+                jj += ss[1];
+                if (
+                  V.OnBoard(ii, jj) &&
+                  this.board[ii][jj] != V.EMPTY &&
+                  this.getColor(ii, jj) == c
+                ) {
+                  shootOk = true;
+                  break;
+                }
+              }
+            }
+          }
+        }
+        if (shootOk) {
+          moves.push(
+            new Move({
+              appear: [],
+              vanish: [
+                new PiPo({ x: i, y: j, c: oppCol, p: this.getPiece(i, j) })
+              ],
+              start: { x: x, y: y },
+              end: { x: i, y: j }
+            })
+          );
+        }
+      }
+    }
+    return moves;
+  }
+
+  getPotentialShieldMoves(sq) {
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    return this.getSlideNJumpMoves(sq, steps);
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+
+  filterValid(moves) {
+    return moves;
+  }
+
+  getCurrentScore() {
+    const c = this.turn;
+    if (this.kingPos[c][0] < 0) return (c == 'w' ? "0-1" : "1-0");
+    if (this.atLeastOneMove()) return "*";
+    // Stalemate, or checkmate: I lose
+    return (c == 'w' ? "0-1" : "1-0");
+  }
+
+  postPlay(move) {
+    const startIdx = (move.appear.length == 0 ? 0 : 1);
+    for (let i = startIdx; i < move.vanish.length; i++) {
+      const v = move.vanish[i];
+      if (v.p == V.KING) this.kingPos[v.c] = [-1, -1];
+    }
+    // King may have moved, or was swapped
+    for (let a of move.appear) {
+      if (a.p == V.KING) {
+        this.kingPos[a.c] = [a.x, a.y];
+        break;
+      }
+    }
+  }
+
+  postUndo(move) {
+    const startIdx = (move.appear.length == 0 ? 0 : 1);
+    for (let i = startIdx; i < move.vanish.length; i++) {
+      const v = move.vanish[i];
+      if (v.p == V.KING) this.kingPos[v.c] = [v.x, v.y];
+    }
+    // King may have moved, or was swapped
+    for (let i = 0; i < move.appear.length; i++) {
+      const a = move.appear[i];
+      if (a.p == V.KING) {
+        const v = move.vanish[i];
+        this.kingPos[a.c] = [v.x, v.y];
+        break;
+      }
+    }
+  }
+
+  static GenRandInitFen(randomness) {
+    if (randomness == 0) {
+      return (
+        "wlqksaui/pppppppp/8/8/8/8/PPPPPPPP/IUASKQLW w 0"
+      );
+    }
+
+    let pieces = { w: new Array(8), b: new Array(8) };
+    // Shuffle pieces on first and last rank
+    for (let c of ["w", "b"]) {
+      if (c == 'b' && randomness == 1) {
+        pieces['b'] = pieces['w'];
+        break;
+      }
+
+      // Get random squares for every piece, totally freely
+      let positions = shuffle(ArrayFun.range(8));
+      const composition = ['w', 'l', 'q', 'k', 's', 'a', 'u', 'i'];
+      for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
+    }
+    return (
+      pieces["b"].join("") +
+      "/pppppppp/8/8/8/8/PPPPPPPP/" +
+      pieces["w"].join("").toUpperCase() + " w 0"
+    );
+  }
+
+  static get VALUES() {
+    // Experimental...
+    return {
+      p: 1,
+      q: 9,
+      l: 5,
+      s: 5,
+      a: 5,
+      u: 5,
+      i: 12,
+      w: 3,
+      k: 1000
+    };
+  }
+
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
+  getNotation(move) {
+    const initialSquare = V.CoordsToSquare(move.start);
+    const finalSquare = V.CoordsToSquare(move.end);
+    if (move.appear.length == 0) {
+      // Archer shooting 'S' or Mutual destruction 'D':
+      return (
+        initialSquare + (move.vanish.length == 1 ? "S" : "D") + finalSquare
+      );
+    }
+    let notation = undefined;
+    const symbol = move.appear[0].p.toUpperCase();
+    if (symbol == 'P')
+      // Pawn: generally ambiguous short notation, so we use full description
+      notation = "P" + initialSquare + finalSquare;
+    else if (['Q', 'K'].includes(symbol))
+      notation = symbol + (move.vanish.length > 1 ? "x" : "") + finalSquare;
+    else {
+      notation = symbol + finalSquare;
+      // Add a capture mark (not describing what is captured...):
+      if (move.vanish.length > 1) notation += "X";
+    }
+    return notation;
+  }
+
+};