From 1006b211894bdc0624e2fd332ac78322bf8ca0ee Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 8 Feb 2021 21:20:57 +0100
Subject: [PATCH] Add Convert variant

---
 client/src/translations/en.js                |   1 +
 client/src/translations/es.js                |   1 +
 client/src/translations/fr.js                |   1 +
 client/src/translations/rules/Convert/en.pug |  30 +++
 client/src/translations/rules/Convert/es.pug |  36 +++
 client/src/translations/rules/Convert/fr.pug |  35 +++
 client/src/translations/variants/en.pug      |   1 +
 client/src/translations/variants/es.pug      |   1 +
 client/src/translations/variants/fr.pug      |   1 +
 client/src/variants/Convert.js               | 267 +++++++++++++++++++
 client/src/variants/Otage.js                 |   2 +-
 client/src/variants/Pacosako.js              |   2 +-
 client/src/variants/Takenmake.js             |   3 +-
 server/db/populate.sql                       |   1 +
 14 files changed, 378 insertions(+), 4 deletions(-)
 create mode 100644 client/src/translations/rules/Convert/en.pug
 create mode 100644 client/src/translations/rules/Convert/es.pug
 create mode 100644 client/src/translations/rules/Convert/fr.pug
 create mode 100644 client/src/variants/Convert.js

diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index cd10d039..ca3b13a6 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -199,6 +199,7 @@ export const translations = {
   "Chinese Chess": "Chinese Chess",
   "Convert & support (v1)": "Convert & support (v1)",
   "Convert & support (v2)": "Convert & support (v2)",
+  "Convert enemy pieces": "Convert enemy pieces",
   "Cross the river": "Cross the river",
   "Dance with the King": "Dance with the King",
   "Dangerous captures": "Dangerous captures",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index 11d64a48..30095bfe 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -199,6 +199,7 @@ export const translations = {
   "Chinese Chess": "Ajedrez chino",
   "Convert & support (v1)": "Convertir & apoyar (v1)",
   "Convert & support (v2)": "Convertir & apoyar (v2)",
+  "Convert enemy pieces": "Convierte piezas opuestas",
   "Cross the river": "Cruza el río",
   "Dance with the King": "Baila con el Rey",
   "Dangerous captures": "Capturas peligrosas",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 4d134fc2..0ebc8175 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -199,6 +199,7 @@ export const translations = {
   "Chinese Chess": "Échecs chinois",
   "Convert & support (v1)": "Convertir & soutenir (v1)",
   "Convert & support (v2)": "Convertir & soutenir (v2)",
+  "Convert enemy pieces": "Convertissez les pièces adverses",
   "Cross the river": "Traversez la rivière",
   "Dance with the King": "Dansez avec le Roi",
   "Dangerous captures": "Captures dangeureuses",
diff --git a/client/src/translations/rules/Convert/en.pug b/client/src/translations/rules/Convert/en.pug
new file mode 100644
index 00000000..164eef14
--- /dev/null
+++ b/client/src/translations/rules/Convert/en.pug
@@ -0,0 +1,30 @@
+p.boxed.
+  Capturing an enemy piece changes its color.
+  The converted piece must be moved immediatly.
+
+p.
+  Each capture transforms the captured piece into a friendly one
+  (changing color). Since it cannot remain on the capturing square,
+  it has to be moved immediately. This newly converted piece can now
+  capture (and thus "convert") another enemy piece, and so on.
+
+p.
+  Castling is impossible if any square traversed by the king is controlled
+  (directly or through a chain). Checks are ignored otherwise.
+
+figure.diagram-container
+  .diagram
+    | fen:qrk2r2/pp4pp/3pp2n/2pb1p1P/2BPPb1n/2B1N1N1/PPP2PPR/RQK5:
+  figcaption Black cannot castle because of the "check" Bxd5, Bxb7, Pxc8
+
+p.
+  The goal is to convert the enemy king. Note that capturing a king is
+  possible only if a king move is then available. However, a piece gives
+  check (concerning castle) even if it's impossible.
+
+figure.diagram-container
+  .diagram
+    | fen:qrk2r2/pp4pp/3pp2n/2pb1p1P/2BPP2n/2B1b1N1/PPn2PPR/Rrqk4:
+  figcaption Black wins from the first diagram: Bxe3, Nxc2, Pxb1=R, Qxc1, Ke1
+
+p If in a chain a converted piece cannot move, then you must reset the move.
diff --git a/client/src/translations/rules/Convert/es.pug b/client/src/translations/rules/Convert/es.pug
new file mode 100644
index 00000000..73f62218
--- /dev/null
+++ b/client/src/translations/rules/Convert/es.pug
@@ -0,0 +1,36 @@
+p.boxed.
+  La captura de una pieza enemiga cambia su color.
+  La pieza convertida debe moverse inmediatamente.
+
+p.
+  Cada captura transforma la pieza capturada en una pieza amiga (cambiando
+  de color). Como no puede permanecer en la casilla de captura,
+  debe moverse de inmediato. La pieza recién convertida puede
+  luego capturar (y así "convertir") una otra pieza opuesta, etc.
+
+p.
+  El enroque es imposible si se controla una casilla cruzada por el rey
+  (directamente o mediante una cadena).
+  Los jaques se ignoran el resto del tiempo.
+
+figure.diagram-container
+  .diagram
+    | fen:qrk2r2/pp4pp/3pp2n/2pb1p1P/2BPPb1n/2B1N1N1/PPP2PPR/RQK5:
+  figcaption Las negras no pueden enrocar debido al "jaque" Bxd5, Bxb7, Pxc8
+
+p.
+  El objetivo es convertir al rey contrario. Tenga en cuenta que la captura
+  de un rey solo es posible si puedes realizar un movimiento de rey después.
+  Sin embargo, una pieza da jaque (en cuanto al enroque)
+  incluso si es imposible.
+
+figure.diagram-container
+  .diagram
+    | fen:qrk2r2/pp4pp/3pp2n/2pb1p1P/2BPP2n/2B1b1N1/PPn2PPR/Rrqk4:
+  figcaption.
+    Las negras ganan desde el primer diagrama:
+    Bxe3, Nxc2, Pxb1 = R, Qxc1, Ke1
+
+p.
+  Si en una cadena una pieza convertida no se puede mover,
+  entonces tienes que empezar de nuevo.
diff --git a/client/src/translations/rules/Convert/fr.pug b/client/src/translations/rules/Convert/fr.pug
new file mode 100644
index 00000000..3e90a8c5
--- /dev/null
+++ b/client/src/translations/rules/Convert/fr.pug
@@ -0,0 +1,35 @@
+p.boxed.
+  Capturer une pièce ennemie change sa couleur.
+  La pièce convertie doit être déplacée immédiatement.
+
+p.
+  Chaque capture transforme la pièce capturée en une pièce amie (changeant
+  de couleur). Puisqu'elle ne peut pas rester sur la case de capture, elle
+  doit tout de suite être déplacée. La pièce nouvellement convertie peut
+  alors capturer (et ainsi "convertir") une autre pièce adverse, etc.
+
+p.
+  Le roque est impossible si une case traversée par le roi est contrôlée
+  (directement ou via une chaîne). Les échecs sont ignorés le reste du temps.
+
+figure.diagram-container
+  .diagram
+    | fen:qrk2r2/pp4pp/3pp2n/2pb1p1P/2BPPb1n/2B1N1N1/PPP2PPR/RQK5:
+  figcaption Les noirs ne peuvent roquer à cause de l'"échec" Bxd5, Bxb7, Pxc8
+
+p.
+  L'objectof est de convertir le roi adverse. Notez qu'une capture de roi
+  n'est possible que si vous pouvez effectuer un coup de roi ensuite.
+  Cependant, une pièce donne échec (concernant le roque)
+  même si c'est impossible.
+
+figure.diagram-container
+  .diagram
+    | fen:qrk2r2/pp4pp/3pp2n/2pb1p1P/2BPP2n/2B1b1N1/PPn2PPR/Rrqk4:
+  figcaption.
+    Les noirs gagnent depuis le premier diagramme:
+    Bxe3, Nxc2, Pxb1=R, Qxc1, Ke1
+
+p.
+  Si dans une chaîne une pièce convertie ne peut pas bouger,
+  alors vous devez recommencer le coup.
diff --git a/client/src/translations/variants/en.pug b/client/src/translations/variants/en.pug
index a895843b..eed4c980 100644
--- a/client/src/translations/variants/en.pug
+++ b/client/src/translations/variants/en.pug
@@ -462,6 +462,7 @@ p.
     "Ambiguous",
     "Bario",
     "Bicolour",
+    "Convert",
     "Evolution",
     "Forward",
     "Fusion",
diff --git a/client/src/translations/variants/es.pug b/client/src/translations/variants/es.pug
index 609ac032..01efa971 100644
--- a/client/src/translations/variants/es.pug
+++ b/client/src/translations/variants/es.pug
@@ -472,6 +472,7 @@ p.
     "Ambiguous",
     "Bario",
     "Bicolour",
+    "Convert",
     "Evolution",
     "Forward",
     "Fusion",
diff --git a/client/src/translations/variants/fr.pug b/client/src/translations/variants/fr.pug
index 6c846970..3566c882 100644
--- a/client/src/translations/variants/fr.pug
+++ b/client/src/translations/variants/fr.pug
@@ -470,6 +470,7 @@ p.
     "Ambiguous",
     "Bario",
     "Bicolour",
+    "Convert",
     "Evolution",
     "Forward",
     "Fusion",
diff --git a/client/src/variants/Convert.js b/client/src/variants/Convert.js
new file mode 100644
index 00000000..297d08db
--- /dev/null
+++ b/client/src/variants/Convert.js
@@ -0,0 +1,267 @@
+import { ChessRules, PiPo, Move } from "@/base_rules";
+import { randInt } from "@/utils/alea";
+
+export class ConvertRules extends ChessRules {
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    // Stack of "last move" only for intermediate chaining
+    this.lastMoveEnd = [null];
+  }
+
+  getBasicMove([sx, sy], [ex, ey], tr) {
+    const L = this.lastMoveEnd.length;
+    const lm = this.lastMoveEnd[L-1];
+    const piece = (!!lm ? lm.p : null);
+    const c = this.turn;
+    if (this.board[ex][ey] == V.EMPTY) {
+      if (!!piece && !tr) tr = { c: c, p: piece }
+      let mv = super.getBasicMove([sx, sy], [ex, ey], tr);
+      if (!!piece) mv.vanish.pop();
+      return mv;
+    }
+    // Capture: initial, or inside a chain
+    const initPiece = (piece || this.getPiece(sx, sy));
+    const oppCol = V.GetOppCol(c);
+    const oppPiece = this.getPiece(ex, ey);
+    let mv = new Move({
+      start: { x: sx, y: sy },
+      end: { x: ex, y: ey },
+      appear: [
+        new PiPo({
+          x: ex,
+          y: ey,
+          c: c,
+          p: (!!tr ? tr.p : initPiece)
+        })
+      ],
+      vanish: [
+        new PiPo({
+          x: ex,
+          y: ey,
+          c: oppCol,
+          p: oppPiece
+        })
+      ]
+    });
+    if (!piece) {
+      // Initial capture
+      mv.vanish.unshift(
+        new PiPo({
+          x: sx,
+          y: sy,
+          c: c,
+          p: initPiece
+        })
+      );
+    }
+    // TODO: This "converted" indication isn't needed in fact,
+    // because it can be deduced from the move itself.
+    mv.end.converted = oppPiece;
+    return mv;
+  }
+
+  getPotentialMovesFrom([x, y], asA) {
+    const L = this.lastMoveEnd.length;
+    if (!!this.lastMoveEnd[L-1]) {
+      if (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y)
+        // A capture was played: wrong square
+        return [];
+      asA = this.lastMoveEnd[L-1].p;
+    }
+    switch (asA || this.getPiece(x, y)) {
+      case V.PAWN: return super.getPotentialPawnMoves([x, y]);
+      case V.ROOK: return super.getPotentialRookMoves([x, y]);
+      case V.KNIGHT: return super.getPotentialKnightMoves([x, y]);
+      case V.BISHOP: return super.getPotentialBishopMoves([x, y]);
+      case V.QUEEN: return super.getPotentialQueenMoves([x, y]);
+      case V.KING: return super.getPotentialKingMoves([x, y]);
+    }
+    return [];
+  }
+
+  getPossibleMovesFrom(sq) {
+    const L = this.lastMoveEnd.length;
+    let asA = undefined;
+    if (!!this.lastMoveEnd[L-1]) {
+      if (
+        sq[0] != this.lastMoveEnd[L-1].x ||
+        sq[1] != this.lastMoveEnd[L-1].y
+      ) {
+        return [];
+      }
+      asA = this.lastMoveEnd[L-1].p;
+    }
+    return this.filterValid(this.getPotentialMovesFrom(sq, asA));
+  }
+
+  isAttacked_aux([x, y], color, explored) {
+    if (explored.some(sq => sq[0] == x && sq[1] == y))
+      // Start of an infinite loop: exit
+      return false;
+    explored.push([x, y]);
+    if (super.isAttacked([x, y], color)) return true;
+    // Maybe indirect "chaining" attack:
+    const myColor = this.turn
+    let res = false;
+    let toCheck = []; //check all but king (no need)
+    // Pawns:
+    const shiftToPawn = (myColor == 'w' ? -1 : 1);
+    for (let yShift of [-1, 1]) {
+      const [i, j] = [x + shiftToPawn, y + yShift];
+      if (
+        V.OnBoard(i, j) &&
+        this.board[i][j] != V.EMPTY &&
+        // NOTE: no need to check color (no enemy pawn can take directly)
+        this.getPiece(i, j) == V.PAWN
+      ) {
+        toCheck.push([i, j]);
+      }
+    }
+    // Knights:
+    V.steps[V.KNIGHT].forEach(s => {
+      const [i, j] = [x + s[0], y + s[1]];
+      if (
+        V.OnBoard(i, j) &&
+        this.board[i][j] != V.EMPTY &&
+        this.getPiece(i, j) == V.KNIGHT
+      ) {
+        toCheck.push([i, j]);
+      }
+    });
+    // Sliders:
+    V.steps[V.ROOK].concat(V.steps[V.BISHOP]).forEach(s => {
+      let [i, j] = [x + s[0], y + s[1]];
+      while (V.OnBoard(i, j) && this.board[i][j] == V.EMPTY) {
+        i += s[0];
+        j += s[1];
+      }
+      if (!V.OnBoard(i, j)) return;
+      const piece = this.getPiece(i, j);
+      if (
+        piece == V.QUEEN ||
+        (piece == V.ROOK && (s[0] == 0 || s[1] == 0)) ||
+        (piece == V.BISHOP && (s[0] != 0 && s[1] != 0))
+      ) {
+        toCheck.push([i, j]);
+      }
+    });
+    for (let ij of toCheck) {
+      if (this.isAttacked_aux(ij, color, explored)) return true;
+    }
+    return false;
+  }
+
+  isAttacked([x, y], color) {
+    let explored = [];
+    return this.isAttacked_aux([x, y], color, explored);
+  }
+
+  filterValid(moves) {
+    // No "checks" (except to forbid castle)
+    return moves;
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+
+  play(move) {
+    move.flags = JSON.stringify(this.aggregateFlags());
+    this.epSquares.push(this.getEpSquare(move));
+    V.PlayOnBoard(this.board, move);
+    if (!move.end.converted) {
+      // Not a capture: change turn
+      this.turn = V.GetOppCol(this.turn);
+      this.movesCount++;
+      this.lastMoveEnd.push(null);
+    }
+    else {
+      this.lastMoveEnd.push(
+        Object.assign({}, move.end, { p: move.end.converted })
+      );
+    }
+    this.postPlay(move);
+  }
+
+  postPlay(move) {
+    const c = (!move.end.converted ? V.GetOppCol(this.turn) : this.turn);
+    const piece = move.appear[0].p;
+    if (piece == V.KING) {
+      this.kingPos[c][0] = move.appear[0].x;
+      this.kingPos[c][1] = move.appear[0].y;
+    }
+    super.updateCastleFlags(move, piece, c);
+  }
+
+  undo(move) {
+    this.disaggregateFlags(JSON.parse(move.flags));
+    this.epSquares.pop();
+    this.lastMoveEnd.pop();
+    V.UndoOnBoard(this.board, move);
+    if (!move.end.converted) {
+      this.turn = V.GetOppCol(this.turn);
+      this.movesCount--;
+    }
+    super.postUndo(move);
+  }
+
+  getCurrentScore() {
+    const color = this.turn;
+    const kp = this.kingPos[color];
+    if (this.getColor(kp[0], kp[1]) != color)
+      return (color == "w" ? "0-1" : "1-0");
+    if (!super.atLeastOneMove()) return "1/2";
+    return "*";
+  }
+
+  getComputerMove() {
+    let initMoves = this.getAllValidMoves();
+    if (initMoves.length == 0) return null;
+    // Loop until valid move is found (no blocked pawn conversion...)
+    while (true) {
+      let moves = JSON.parse(JSON.stringify(initMoves));
+      let mvArray = [];
+      let mv = null;
+      // Just play random moves (for now at least. TODO?)
+      while (moves.length > 0) {
+        mv = moves[randInt(moves.length)];
+        mvArray.push(mv);
+        this.play(mv);
+        if (!!mv.end.converted)
+          // A piece was just converted
+          moves = this.getPotentialMovesFrom([mv.end.x, mv.end.y]);
+        else break;
+      }
+      for (let i = mvArray.length - 1; i >= 0; i--) this.undo(mvArray[i]);
+      if (!mv.end.released) return (mvArray.length > 1 ? mvArray : mvArray[0]);
+    }
+    return null; //never reached
+  }
+
+  getNotation(move) {
+    if (move.appear.length == 2 && move.appear[0].p == V.KING)
+      return (move.end.y < move.start.y ? "0-0-0" : "0-0");
+    const c = this.turn;
+    const L = this.lastMoveEnd.length;
+    const lm = this.lastMoveEnd[L-1];
+    const piece = (!lm ? move.appear[0].p : lm.p);
+    // Basic move notation:
+    let notation = piece.toUpperCase();
+    if (
+      this.board[move.end.x][move.end.y] != V.EMPTY ||
+      (piece == V.PAWN && move.start.y != move.end.y)
+    ) {
+      notation += "x";
+    }
+    const finalSquare = V.CoordsToSquare(move.end);
+    notation += finalSquare;
+
+    // Add potential promotion indications:
+    const firstLastRank = (c == 'w' ? [7, 0] : [0, 7]);
+    if (move.end.x == firstLastRank[1] && piece == V.PAWN)
+      notation += "=" + move.appear[0].p.toUpperCase();
+    return notation;
+  }
+
+};
diff --git a/client/src/variants/Otage.js b/client/src/variants/Otage.js
index 451059aa..79157157 100644
--- a/client/src/variants/Otage.js
+++ b/client/src/variants/Otage.js
@@ -355,7 +355,7 @@ export class OtageRules extends ChessRules {
     }
     let baseMoves = [];
     const c = this.turn;
-    switch (piece || this.getPiece(x, y)) {
+    switch (piece) {
       case V.PAWN: {
         const firstRank = (c == 'w' ? 7 : 0);
         baseMoves = this.getPotentialPawnMoves([x, y]).filter(m => {
diff --git a/client/src/variants/Pacosako.js b/client/src/variants/Pacosako.js
index 0b27be4a..4745f631 100644
--- a/client/src/variants/Pacosako.js
+++ b/client/src/variants/Pacosako.js
@@ -372,7 +372,7 @@ export class PacosakoRules extends ChessRules {
     }
     let baseMoves = [];
     const c = this.turn;
-    switch (piece || this.getPiece(x, y)) {
+    switch (piece) {
       case V.PAWN: {
         const firstRank = (c == 'w' ? 7 : 0);
         baseMoves = this.getPotentialPawnMoves([x, y]).filter(m => {
diff --git a/client/src/variants/Takenmake.js b/client/src/variants/Takenmake.js
index 9549c2a9..d37edf6c 100644
--- a/client/src/variants/Takenmake.js
+++ b/client/src/variants/Takenmake.js
@@ -13,10 +13,9 @@ export class TakenmakeRules extends ChessRules {
     const L = this.lastMoveEnd.length;
     if (!asA && !!this.lastMoveEnd[L-1]) {
       asA = this.lastMoveEnd[L-1].p;
-      if (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y) {
+      if (x != this.lastMoveEnd[L-1].x || y != this.lastMoveEnd[L-1].y)
         // A capture was played: wrong square
         return [];
-      }
     }
     let moves = [];
     const piece = this.getPiece(x, y);
diff --git a/server/db/populate.sql b/server/db/populate.sql
index 9b7842be..f9c9a332 100644
--- a/server/db/populate.sql
+++ b/server/db/populate.sql
@@ -47,6 +47,7 @@ insert or ignore into Variants (name, description) values
   ('Circular', 'Run forward'),
   ('Clorange', 'A Clockwork Orange'),
   ('Colorbound', 'The colorbound clobberers'),
+  ('Convert', 'Convert enemy pieces'),
   ('Coregal', 'Two royal pieces'),
   ('Coronation', 'Long live the Queen'),
   ('Crazyhouse', 'Captures reborn'),
-- 
2.44.0