Add Rococo variant
[vchess.git] / client / src / variants / Rococo.js
diff --git a/client/src/variants/Rococo.js b/client/src/variants/Rococo.js
new file mode 100644 (file)
index 0000000..8489ae9
--- /dev/null
@@ -0,0 +1,714 @@
+import { ChessRules, PiPo, Move } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { shuffle } from "@/utils/alea";
+
+export class RococoRules extends ChessRules {
+  static get HasFlags() {
+    return false;
+  }
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  static get PIECES() {
+    return ChessRules.PIECES.concat([V.IMMOBILIZER]);
+  }
+
+  getPpath(b) {
+    if (b[1] == "m")
+      //'m' for Immobilizer (I is too similar to 1)
+      return "Rococo/" + b;
+    return b; //usual piece
+  }
+
+  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 m.appear[0].c + m.appear[0].p;
+  }
+
+  setOtherVariables(fen) {
+    // No castling, but checks, so keep track of kings
+    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));
+            if (!isNaN(num)) k += num - 1;
+          }
+        }
+        k++;
+      }
+    }
+    // Local stack of swaps:
+    this.smoves = [];
+    const smove = V.ParseFen(fen).smove;
+    if (smove == "-") this.smoves.push(null);
+    else {
+      this.smoves.push({
+        start: ChessRules.SquareToCoords(smove.substr(0, 2)),
+        end: ChessRules.SquareToCoords(smove.substr(2))
+      });
+    }
+  }
+
+  static ParseFen(fen) {
+    return Object.assign(
+      ChessRules.ParseFen(fen),
+      { smove: fen.split(" ")[3] }
+    );
+  }
+
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const fenParts = fen.split(" ");
+    if (fenParts.length != 4) return false;
+    if (fenParts[3] != "-" && !fenParts[3].match(/^([a-h][1-8]){2}$/))
+      return false;
+    return true;
+  }
+
+  getSmove(move) {
+    if (move.appear.length == 2)
+      return { start: move.start, end: move.end };
+    return null;
+  }
+
+  static get size() {
+    // Add the "capturing edge"
+    return { x: 10, y: 10 };
+  }
+
+  static get IMMOBILIZER() {
+    return "m";
+  }
+  // Although other pieces keep their names here for coding simplicity,
+  // keep in mind that:
+  //  - a "rook" is a swapper, exchanging positions and "capturing" by
+  //             mutual destruction only.
+  //  - a "knight" is a long-leaper, capturing as in draughts
+  //  - a "bishop" is a chameleon, capturing as its prey
+  //  - a "queen" is a withdrawer+advancer, capturing by moving away from
+  //              pieces or advancing in front of them.
+
+  // Is piece on square (x,y) immobilized?
+  isImmobilized([x, y]) {
+    const piece = this.getPiece(x, y);
+    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
+      ) {
+        const oppPiece = this.getPiece(i, j);
+        if (oppPiece == V.IMMOBILIZER) return [i, j];
+        // Only immobilizers are immobilized by chameleons:
+        if (oppPiece == V.BISHOP && piece == V.IMMOBILIZER) return [i, j];
+      }
+    }
+    return null;
+  }
+
+  static OnEdge(x, y) {
+    return x == 0 || y == 0 || x == V.size.x - 1 || y == V.size.y - 1;
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    // Pre-check: is thing on this square immobilized?
+    const imSq = this.isImmobilized([x, y]);
+    if (!!imSq) {
+      // Only option is suicide:
+      return [
+        new Move({
+          start: { x: x, y: y },
+          end: { x: imSq[0], y: imSq[1] },
+          appear: [],
+          vanish: [
+            new PiPo({
+              x: x,
+              y: y,
+              c: this.getColor(x, y),
+              p: this.getPiece(x, y)
+            })
+          ]
+        })
+      ];
+    }
+    let moves = [];
+    switch (this.getPiece(x, y)) {
+      case V.IMMOBILIZER:
+        moves = this.getPotentialImmobilizerMoves([x, y]);
+        break;
+      default:
+        moves = super.getPotentialMovesFrom([x, y]);
+    }
+    // Post-processing: prune redundant non-minimal capturing moves,
+    // and non-capturing moves ending on the edge:
+    moves.forEach(m => {
+      // Useful precomputation
+      m.dist = Math.abs(m.end.x - m.start.x) + Math.abs(m.end.y - m.start.y);
+    });
+    return moves.filter(m => {
+      if (!V.OnEdge(m.end.x, m.end.y)) return true;
+      // End on the edge:
+      if (m.vanish.length == 1) return false;
+      // Capture or swap: only captures get filtered
+      if (m.appear.length == 2) return true;
+      // Can we find other moves with a shorter path to achieve the same
+      // capture? Apply to queens and knights.
+      if (
+        moves.some(mv => {
+          return (
+            mv.dist < m.dist &&
+            mv.vanish.length == m.vanish.length &&
+            mv.vanish.every(v => {
+              return m.vanish.some(vv => {
+                return (
+                  vv.x == v.x && vv.y == v.y && vv.c == v.c && vv.p == v.p
+                );
+              });
+            })
+          );
+        })
+      ) {
+        return false;
+      }
+      return true;
+    });
+    // NOTE: not removing "dist" field; shouldn't matter much...
+  }
+
+  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 king can take on occupied square:
+      if (piece == V.KING && 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 oppCol = V.GetOppCol(this.turn);
+    let moves = [];
+    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]));
+        else {
+          // Try to leap over:
+          const [ii, jj] = [i + step[0], j + step[1]];
+          if (V.OnBoard(ii, jj) && this.getColor(ii, jj) == oppCol)
+            moves.push(this.getBasicMove([x, y], [ii, jj]));
+        }
+      }
+    });
+    return moves;
+  }
+
+  // NOTE: not really captures, but let's keep the name
+  getRookCaptures([x, y], byChameleon) {
+    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);
+        if (!byChameleon || oppPiece == V.ROOK) {
+          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]) {
+            // 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;
+  }
+
+  // Swapper
+  getPotentialRookMoves(sq) {
+    return super.getPotentialQueenMoves(sq).concat(this.getRookCaptures(sq));
+  }
+
+  getKnightCaptures(startSquare, byChameleon) {
+    // 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 [x, y] = [startSquare[0], startSquare[1]];
+    const piece = this.getPiece(x, y); //might be a chameleon!
+    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 ||
+        (!!byChameleon && this.getPiece(i, j) != V.KNIGHT)
+      ) {
+        continue;
+      }
+      // last(thing), cur(thing) : stop if "cur" is our color,
+      // or beyond board limits, or if "last" isn't empty and cur neither.
+      // Otherwise, if cur is empty then add move until cur square;
+      // if cur is occupied then stop if !!byChameleon and the square not
+      // occupied by a leaper.
+      let last = [i, j];
+      let cur = [i + step[0], j + step[1]];
+      let vanished = [new PiPo({ x: x, y: y, c: color, p: piece })];
+      while (V.OnBoard(cur[0], cur[1])) {
+        if (this.board[last[0]][last[1]] != V.EMPTY) {
+          const oppPiece = this.getPiece(last[0], last[1]);
+          if (!!byChameleon && oppPiece != V.KNIGHT) continue outerLoop;
+          // Something to eat:
+          vanished.push(
+            new PiPo({ x: last[0], y: last[1], c: oppCol, p: oppPiece })
+          );
+        }
+        if (this.board[cur[0]][cur[1]] != V.EMPTY) {
+          if (
+            this.getColor(cur[0], cur[1]) == color ||
+            this.board[last[0]][last[1]] != V.EMPTY
+          ) {
+            //TODO: redundant test
+            continue outerLoop;
+          }
+        } else {
+          moves.push(
+            new Move({
+              appear: [new PiPo({ x: cur[0], y: cur[1], c: color, p: piece })],
+              vanish: JSON.parse(JSON.stringify(vanished)), //TODO: required?
+              start: { x: x, y: y },
+              end: { x: cur[0], y: cur[1] }
+            })
+          );
+        }
+        last = [last[0] + step[0], last[1] + step[1]];
+        cur = [cur[0] + step[0], cur[1] + step[1]];
+      }
+    }
+    return moves;
+  }
+
+  // Long-leaper
+  getPotentialKnightMoves(sq) {
+    return super.getPotentialQueenMoves(sq).concat(this.getKnightCaptures(sq));
+  }
+
+  // Chameleon
+  getPotentialBishopMoves([x, y]) {
+    const oppCol = V.GetOppCol(this.turn);
+    let moves = super
+      .getPotentialQueenMoves([x, y])
+      .concat(this.getKnightCaptures([x, y], "asChameleon"))
+      .concat(this.getRookCaptures([x, y], "asChameleon"));
+    // No "king capture" because king cannot remain under check
+    this.addQueenCaptures(moves, "asChameleon");
+    // Also add pawn captures (as a pawn):
+    const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    adjacentSteps.forEach(step => {
+      const [i, j] = [x + step[0], y + step[1]];
+      const [ii, jj] = [i + step[0], j + step[1]];
+      // Try to leap over (i,j):
+      if (
+        V.OnBoard(ii, jj) &&
+        this.board[i][j] != V.EMPTY &&
+        this.board[ii][jj] != V.EMPTY &&
+        this.getColor(ii, jj) == oppCol &&
+        this.getPiece(ii, jj) == V.PAWN
+      ) {
+        moves.push(this.getBasicMove([x, y], [ii, jj]));
+      }
+    });
+    // Post-processing: merge similar moves, concatenating vanish arrays
+    let mergedMoves = {};
+    moves.forEach(m => {
+      const key = m.end.x + V.size.x * m.end.y;
+      if (!mergedMoves[key]) mergedMoves[key] = m;
+      else {
+        for (let i = 1; i < m.vanish.length; i++)
+          mergedMoves[key].vanish.push(m.vanish[i]);
+      }
+    });
+    return Object.values(mergedMoves);
+  }
+
+  addQueenCaptures(moves, byChameleon) {
+    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 &&
+        (!byChameleon || this.getPiece(i, j) == V.QUEEN)
+      ) {
+        capturingDirStart[step[0] + "_" + step[1]] = this.getPiece(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) {
+        const [i, j] = [x - step[0], y - step[1]];
+        m.vanish.push(
+          new PiPo({
+            x: i,
+            y: j,
+            p: capture,
+            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 &&
+        (!byChameleon || this.getPiece(i, j) == V.QUEEN)
+      ) {
+        m.vanish.push(
+          new PiPo({
+            x: i,
+            y: j,
+            p: this.getPiece(i, j),
+            c: oppCol
+          })
+        );
+      }
+    });
+  }
+
+  // Withdrawer + advancer: "pushme-pullyu"
+  getPotentialQueenMoves(sq) {
+    let moves = super.getPotentialQueenMoves(sq);
+    this.addQueenCaptures(moves);
+    return moves;
+  }
+
+  getPotentialImmobilizerMoves(sq) {
+    // Immobilizer doesn't capture
+    return super.getPotentialQueenMoves(sq);
+  }
+
+  // Does m2 un-do m1 ? (to disallow undoing swaps)
+  oppositeMoves(m1, m2) {
+    return (
+      !!m1 &&
+      m2.appear.length == 2 &&
+      m1.start.x == m2.start.x &&
+      m1.end.x == m2.end.x &&
+      m1.start.y == m2.start.y &&
+      m1.end.y == m2.end.y
+    );
+  }
+
+  filterValid(moves) {
+    if (moves.length == 0) return [];
+    const color = this.turn;
+    return (
+      super.filterValid(
+        moves.filter(m => {
+          const L = this.smoves.length; //at least 1: init from FEN
+          return !this.oppositeMoves(this.smoves[L - 1], m);
+        })
+      )
+    );
+  }
+
+  // isAttacked() is OK because the immobilizer doesn't take
+
+  isAttackedByPawn([x, y], color) {
+    // Attacked if an enemy pawn stands just behind an immediate obstacle:
+    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]];
+      const [ii, jj] = [i + step[0], j + step[1]];
+      if (
+        V.OnBoard(ii, jj) &&
+        this.board[i][j] != V.EMPTY &&
+        this.board[ii][jj] != V.EMPTY &&
+        this.getColor(ii, jj) == color &&
+        this.getPiece(ii, jj) == V.PAWN &&
+        !this.isImmobilized([ii, jj])
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  isAttackedByRook([x, y], color) {
+    // The only way a swapper can take is by mutual destruction when the
+    // enemy piece stands just next:
+    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) == color &&
+        this.getPiece(i, j) == V.ROOK &&
+        !this.isImmobilized([i, j])
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  isAttackedByKnight([x, y], color) {
+    // Square (x,y) must be on same line as a knight,
+    // and there must be empty square(s) behind.
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    outerLoop: for (let step of steps) {
+      const [i0, j0] = [x + step[0], y + step[1]];
+      if (V.OnBoard(i0, j0) && this.board[i0][j0] == V.EMPTY) {
+        // Try in opposite direction:
+        let [i, j] = [x - step[0], y - step[1]];
+        while (V.OnBoard(i, j)) {
+          while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+            i -= step[0];
+            j -= step[1];
+          }
+          if (V.OnBoard(i, j)) {
+            if (this.getColor(i, j) == color) {
+              if (
+                this.getPiece(i, j) == V.KNIGHT &&
+                !this.isImmobilized([i, j])
+              )
+                return true;
+              continue outerLoop;
+            }
+            // [else] Our color,
+            // could be captured *if there was an empty space*
+            if (this.board[i + step[0]][j + step[1]] != V.EMPTY)
+              continue outerLoop;
+            i -= step[0];
+            j -= step[1];
+          }
+        }
+      }
+    }
+    return false;
+  }
+
+  isAttackedByBishop([x, y], color) {
+    // We cheat a little here: since this function is used exclusively for
+    // the king, it's enough to check the immediate surrounding of the square.
+    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) == color &&
+        this.getPiece(i, j) == V.BISHOP &&
+        !this.isImmobilized([i, j])
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  isAttackedByQueen([x, y], color) {
+    // Is there a queen in view?
+    const adjacentSteps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    for (let step of adjacentSteps) {
+      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.getPiece(i, j) == V.QUEEN
+      ) {
+        // Two cases: the queen is at 2 steps at least, or just close
+        // but maybe with enough space behind to withdraw.
+        let attacked = false;
+        if (i == x + step[0] && j == y + step[1]) {
+          const [ii, jj] = [i + step[0], j + step[1]];
+          if (V.OnBoard(ii, jj) && this.board[ii][jj] == V.EMPTY)
+            attacked = true;
+        }
+        else attacked = true;
+        if (attacked && !this.isImmobilized([i, j])) return true;
+      }
+    }
+    return false;
+  }
+
+  isAttackedByKing([x, y], color) {
+    const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
+    for (let step of steps) {
+      let rx = x + step[0],
+          ry = y + step[1];
+      if (
+        V.OnBoard(rx, ry) &&
+        this.getPiece(rx, ry) === V.KING &&
+        this.getColor(rx, ry) == color &&
+        !this.isImmobilized([rx, ry])
+      ) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  static GenRandInitFen(randomness) {
+    if (randomness == 0) {
+      return (
+        "91/1rnbkqbnm1/1pppppppp1/91/91/91/91/1PPPPPPPP1/1MNBQKBNR1/91 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 = ['r', 'm', 'n', 'n', 'q', 'q', 'b', 'k'];
+      for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
+    }
+    return (
+      "91/1" + pieces["b"].join("") +
+      "1/1pppppppp1/91/91/91/91/1PPPPPPPP1/1" +
+      pieces["w"].join("").toUpperCase() + "1/91 w 0 -"
+    );
+  }
+
+  getSmoveFen() {
+    const L = this.smoves.length;
+    return (
+      !this.smoves[L - 1]
+        ? "-"
+        : ChessRules.CoordsToSquare(this.smoves[L - 1].start) +
+          ChessRules.CoordsToSquare(this.smoves[L - 1].end)
+    );
+  }
+
+  getFen() {
+    return super.getFen() + " " + this.getSmoveFen();
+  }
+
+  getFenForRepeat() {
+    return super.getFenForRepeat() + "_" + this.getSmoveFen();
+  }
+
+  postPlay(move) {
+    super.postPlay(move);
+    this.smoves.push(this.getSmove(move));
+  }
+
+  postUndo(move) {
+    super.postUndo(move);
+    this.smoves.pop();
+  }
+
+  static get VALUES() {
+    return {
+      p: 1,
+      r: 2,
+      n: 5,
+      b: 3,
+      q: 5,
+      m: 5,
+      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) {
+      // Suicide 'S' or mutual destruction 'D':
+      return (
+        initialSquare + (move.vanish.length == 1 ? "S" : "D" + finalSquare)
+      );
+    }
+    let notation = undefined;
+    if (move.appear[0].p == V.PAWN) {
+      // Pawn: generally ambiguous short notation, so we use full description
+      notation = "P" + initialSquare + finalSquare;
+    } else if (move.appear[0].p == V.KING)
+      notation = "K" + (move.vanish.length > 1 ? "x" : "") + finalSquare;
+    else notation = move.appear[0].p.toUpperCase() + finalSquare;
+    // Add a capture mark (not describing what is captured...):
+    if (move.vanish.length > 1 && move.appear[0].p != V.KING) notation += "X";
+    return notation;
+  }
+};