From 78d64531113d4b5045ff588dd43f301a332ebae8 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Sat, 22 Feb 2020 23:45:13 +0100
Subject: [PATCH] Draft Recycle variant

---
 client/src/base_rules.js                      |  11 +-
 client/src/translations/en.js                 |   1 +
 client/src/translations/es.js                 |   1 +
 client/src/translations/fr.js                 |   1 +
 .../src/translations/rules/Crazyhouse/en.pug  |   2 +-
 client/src/translations/rules/Recycle/en.pug  |  29 ++
 client/src/translations/rules/Recycle/es.pug  |  32 ++
 client/src/translations/rules/Recycle/fr.pug  |  32 ++
 client/src/variants/Antiking.js               |   1 -
 client/src/variants/Baroque.js                |   4 +-
 client/src/variants/Crazyhouse.js             |   8 +-
 client/src/variants/Enpassant.js              |  70 ++---
 client/src/variants/Recycle.js                | 278 ++++++++++++++++++
 server/db/populate.sql                        |   1 +
 14 files changed, 418 insertions(+), 53 deletions(-)
 create mode 100644 client/src/translations/rules/Recycle/en.pug
 create mode 100644 client/src/translations/rules/Recycle/es.pug
 create mode 100644 client/src/translations/rules/Recycle/fr.pug
 create mode 100644 client/src/variants/Recycle.js

diff --git a/client/src/base_rules.js b/client/src/base_rules.js
index 5bf47f76..7e4057bb 100644
--- a/client/src/base_rules.js
+++ b/client/src/base_rules.js
@@ -968,6 +968,8 @@ export const ChessRules = class ChessRules {
   // After move is played, update variables + flags
   updateVariables(move) {
     let piece = undefined;
+    // TODO: update variables before move is played, and just use this.turn ?
+    // (doesn't work in general, think MarseilleChess)
     let c = undefined;
     if (move.vanish.length >= 1) {
       // Usual case, something is moved
@@ -978,9 +980,8 @@ export const ChessRules = class ChessRules {
       piece = move.appear[0].p;
       c = move.appear[0].c;
     }
-    if (c == "c") {
-      //if (!["w","b"].includes(c))
-      // 'c = move.vanish[0].c' doesn't work for Checkered
+    if (!['w','b'].includes(c)) {
+      // Checkered, for example
       c = V.GetOppCol(this.turn);
     }
     const firstRank = c == "w" ? V.size.x - 1 : 0;
@@ -1265,10 +1266,10 @@ export const ChessRules = class ChessRules {
         // Capture
         const startColumn = V.CoordToColumn(move.start.y);
         notation = startColumn + "x" + finalSquare;
-      } //no capture
+      }
       else notation = finalSquare;
       if (move.appear.length > 0 && move.appear[0].p != V.PAWN)
-        //promotion
+        // Promotion
         notation += "=" + move.appear[0].p.toUpperCase();
       return notation;
     }
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index f44b82f5..93a0865f 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -143,6 +143,7 @@ export const translations = {
   "Lose all pieces": "Lose all pieces",
   "Move twice": "Move twice",
   "Pawns move diagonally": "Pawns move diagonally",
+  "Reuse pieces": "Reuse pieces",
   "Reverse captures": "Reverse captures",
   "Shared pieces": "Shared pieces",
   "Standard rules": "Standard rules"
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index ea576c17..792a208c 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -143,6 +143,7 @@ export const translations = {
   "Lose all pieces": "Perder todas las piezas",
   "Move twice": "Mover dos veces",
   "Pawns move diagonally": "Peones se mueven en diagonal",
+  "Reuse pieces": "Reutilizar piezas",
   "Reverse captures": "Capturas invertidas",
   "Shared pieces": "Piezas compartidas",
   "Standard rules": "Reglas estandar"
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 0f09a092..a79597b8 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -143,6 +143,7 @@ export const translations = {
   "Lose all pieces": "Perdez toutes les pièces",
   "Move twice": "Jouer deux coups",
   "Pawns move diagonally": "Les pions vont en diagonale",
+  "Reuse pieces": "Réutiliser les pièces",
   "Reverse captures": "Captures inversées",
   "Shared pieces": "Pièces partagées",
   "Standard rules": "Règles usuelles"
diff --git a/client/src/translations/rules/Crazyhouse/en.pug b/client/src/translations/rules/Crazyhouse/en.pug
index fee3cd93..adb00340 100644
--- a/client/src/translations/rules/Crazyhouse/en.pug
+++ b/client/src/translations/rules/Crazyhouse/en.pug
@@ -27,5 +27,5 @@ h3 More information
 
 p
   | This variant is very popular, a possible starting point is 
-  a(href="https://www.chessvariants.com/other.dir/crazyhouse.html") lichess.org
+  a(href="https://lichess.org/variant/crazyhouse") lichess.org
   | .
diff --git a/client/src/translations/rules/Recycle/en.pug b/client/src/translations/rules/Recycle/en.pug
new file mode 100644
index 00000000..b7754dc1
--- /dev/null
+++ b/client/src/translations/rules/Recycle/en.pug
@@ -0,0 +1,29 @@
+p.boxed
+  | Capturing your own pieces is possible: captured units
+  | can be landed anywhere instead of moving a piece.
+
+p
+  | Orthodox rules apply, with only one change:
+  | at each turn you can choose to eat one of your pieces, augmenting your
+  | "reserve" of waiting pieces with this figure. At every move, you may
+  | choose to land one of your reserve pieces anywhere on the board
+  | (except first and last ranks for pawns), instead of playing a regular move.
+
+p.
+  Move notation: an arobase '@' indicate piece landing. For example B@d4 means
+  a bishop rebirth on d4 square, and @f3 is a pawn landing on f3.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkb1r/ppppnppp/8/4N3/3p4/8/PPPPPPPP/1RBQKBNR:
+  figcaption After 1.Rxb1 Nxe7 2.N@e5 @d4
+
+p.
+  Note: when a pawn reaches the 8th(1st) rank, it will get removed from the board.
+  It will not be replaced by another piece. It does not promote.
+
+h3 Source
+
+p
+  a(href="https://www.chessvariants.com/difftaking.dir/recyclechess.html") Recycle chess
+  | &nbsp;on chessvariants.com.
diff --git a/client/src/translations/rules/Recycle/es.pug b/client/src/translations/rules/Recycle/es.pug
new file mode 100644
index 00000000..775c57f0
--- /dev/null
+++ b/client/src/translations/rules/Recycle/es.pug
@@ -0,0 +1,32 @@
+p.boxed
+  | Puedes capturar tus propias piezas: estas pueden ser
+  | colocándo a cualquier casilla en lugar de moverlo.
+
+p.
+  Se aplican reglas ortodoxas, con un solo cambio:
+  cada turno puedes elegir comer una de tus propias piezas,
+  aumentando su "reserva" de piezas de espera con esta unidad.
+  Con cada movimiento puedes elegir poner una de estas piezas en reserva
+  (en cualquier lugar, excepto las filas 1 y 8 para los peones) en lugar
+  de jugar una jugada habitual.
+
+p.
+  Notación: un signo '@' indica la llegada de una pieza de reserva.
+  Por ejemplo, B@d4 significa que un alfil se mete en d4, y @f3 significa
+  renacimiento de peón en f3.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkb1r/ppppnppp/8/4N3/3p4/8/PPPPPPPP/1RBQKBNR:
+  figcaption Después de 1.Rxb1 Nxe7 2.N@e5 @d4
+
+p.
+  Nota: cuando un peón alcanza la 8va(1ra) fila, se retira del juego.
+  No se reemplaza por otra pieza. No hay promoción
+
+h3 Fuente
+
+p
+  | La
+  a(href="https://www.chessvariants.com/difftaking.dir/recyclechess.html") variante Reciclaje
+  | &nbsp;en chessvariants.com.
diff --git a/client/src/translations/rules/Recycle/fr.pug b/client/src/translations/rules/Recycle/fr.pug
new file mode 100644
index 00000000..679fd79d
--- /dev/null
+++ b/client/src/translations/rules/Recycle/fr.pug
@@ -0,0 +1,32 @@
+p.boxed
+  | Vous pouvez capturer vos propres pièces : celles-ci peuvent ensuite être
+  | parachutées n'importe où au lieu d'effectuer un déplacement.
+
+p.
+  Les règles orthodoxes s'appliquent, avec un seul changement :
+  à chaque tour vous pouvez choisir de manger une de vos propres pièces,
+  augmentant votre "réserve" de pièces en attente avec cette unité.
+  À chaque coup vous pouvez choisir de poser une de ces pièces en réserve
+  (n'importe où, sauf rangées 1 et 8 pour les pions) au lieu de jouer un
+  coup habituel.
+
+p.
+  Notation : une arobase '@' indique l'arrivée d'une pièce de réserve.
+  Par exemple, B@d4 signifie un parachutage de fou en d4, et @f3 une
+  renaissance de pion en f3.
+
+figure.diagram-container
+  .diagram
+    | fen:rnbqkb1r/ppppnppp/8/4N3/3p4/8/PPPPPPPP/1RBQKBNR:
+  figcaption After 1.Rxb1 Nxe7 2.N@e5 @d4
+
+p.
+  Note : quand un pion atteint la 8eme(1ere) rangée, il est retiré du jeu.
+  Il n'est pas remplacé par une autre pièce. Il n'y a pas de promotion.
+
+h3 Source
+
+p
+  | La 
+  a(href="https://www.chessvariants.com/difftaking.dir/recyclechess.html") variante Recyclage
+  | &nbsp;sur chessvariants.com.
diff --git a/client/src/variants/Antiking.js b/client/src/variants/Antiking.js
index a13247a1..3b1b36e3 100644
--- a/client/src/variants/Antiking.js
+++ b/client/src/variants/Antiking.js
@@ -131,7 +131,6 @@ export const VariantRules = class AntikingRules extends ChessRules {
 
   getCurrentScore() {
     if (this.atLeastOneMove())
-      // game not over
       return "*";
 
     const color = this.turn;
diff --git a/client/src/variants/Baroque.js b/client/src/variants/Baroque.js
index 79fc73ce..5687e22c 100644
--- a/client/src/variants/Baroque.js
+++ b/client/src/variants/Baroque.js
@@ -203,7 +203,6 @@ export const VariantRules = class BaroqueRules extends ChessRules {
     return moves;
   }
 
-  // Long-leaper
   getKnightCaptures(startSquare, byChameleon) {
     // Look in every direction for captures
     const steps = V.steps[V.ROOK].concat(V.steps[V.BISHOP]);
@@ -271,6 +270,7 @@ export const VariantRules = class BaroqueRules extends ChessRules {
     return super.getPotentialQueenMoves(sq).concat(this.getKnightCaptures(sq));
   }
 
+  // Chameleon
   getPotentialBishopMoves([x, y]) {
     let moves = super
       .getPotentialQueenMoves([x, y])
@@ -297,7 +297,6 @@ export const VariantRules = class BaroqueRules extends ChessRules {
     return moves;
   }
 
-  // Withdrawer
   addQueenCaptures(moves, byChameleon) {
     if (moves.length == 0) return;
     const [x, y] = [moves[0].start.x, moves[0].start.y];
@@ -341,6 +340,7 @@ export const VariantRules = class BaroqueRules extends ChessRules {
     });
   }
 
+  // Withdrawer
   getPotentialQueenMoves(sq) {
     let moves = super.getPotentialQueenMoves(sq);
     this.addQueenCaptures(moves);
diff --git a/client/src/variants/Crazyhouse.js b/client/src/variants/Crazyhouse.js
index 89b6f830..d7d82b8c 100644
--- a/client/src/variants/Crazyhouse.js
+++ b/client/src/variants/Crazyhouse.js
@@ -208,8 +208,9 @@ export const VariantRules = class CrazyhouseRules extends ChessRules {
   }
 
   static get SEARCH_DEPTH() {
+    // High branching factor
     return 2;
-  } //high branching factor
+  }
 
   evalPosition() {
     let evaluation = super.evalPosition();
@@ -229,9 +230,4 @@ export const VariantRules = class CrazyhouseRules extends ChessRules {
       move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : "";
     return piece + "@" + V.CoordsToSquare(move.end);
   }
-
-  getLongNotation(move) {
-    if (move.vanish.length > 0) return super.getLongNotation(move);
-    return "@" + V.CoordsToSquare(move.end);
-  }
 };
diff --git a/client/src/variants/Enpassant.js b/client/src/variants/Enpassant.js
index 50f97387..374a620c 100644
--- a/client/src/variants/Enpassant.js
+++ b/client/src/variants/Enpassant.js
@@ -74,53 +74,47 @@ export const VariantRules = class EnpassantRules extends ChessRules {
     let moves = [];
     const [sizeX, sizeY] = [V.size.x, V.size.y];
     const shiftX = color == "w" ? -1 : 1;
-    const firstRank = color == "w" ? sizeX - 1 : 0;
     const startRank = color == "w" ? sizeX - 2 : 1;
     const lastRank = color == "w" ? 0 : sizeX - 1;
-    const pawnColor = this.getColor(x, y); //can be different for checkered
 
-    // NOTE: next condition is generally true (no pawn on last rank)
-    if (x + shiftX >= 0 && x + shiftX < sizeX) {
-      const finalPieces =
-        x + shiftX == lastRank
-          ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]
-          : [V.PAWN];
-      // One square forward
-      if (this.board[x + shiftX][y] == V.EMPTY) {
+    const finalPieces =
+      x + shiftX == lastRank
+        ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]
+        : [V.PAWN];
+    // One square forward
+    if (this.board[x + shiftX][y] == V.EMPTY) {
+      for (let piece of finalPieces) {
+        moves.push(
+          this.getBasicMove([x, y], [x + shiftX, y], {
+            c: color,
+            p: piece
+          })
+        );
+      }
+      if (
+        x == startRank &&
+        this.board[x + 2 * shiftX][y] == V.EMPTY
+      ) {
+        // Two squares jump
+        moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
+      }
+    }
+    // Captures
+    for (let shiftY of [-1, 1]) {
+      if (
+        y + shiftY >= 0 &&
+        y + shiftY < sizeY &&
+        this.board[x + shiftX][y + shiftY] != V.EMPTY &&
+        this.canTake([x, y], [x + shiftX, y + shiftY])
+      ) {
         for (let piece of finalPieces) {
           moves.push(
-            this.getBasicMove([x, y], [x + shiftX, y], {
-              c: pawnColor,
+            this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
+              c: color,
               p: piece
             })
           );
         }
-        // Next condition because pawns on 1st rank can generally jump
-        if (
-          [startRank, firstRank].includes(x) &&
-          this.board[x + 2 * shiftX][y] == V.EMPTY
-        ) {
-          // Two squares jump
-          moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
-        }
-      }
-      // Captures
-      for (let shiftY of [-1, 1]) {
-        if (
-          y + shiftY >= 0 &&
-          y + shiftY < sizeY &&
-          this.board[x + shiftX][y + shiftY] != V.EMPTY &&
-          this.canTake([x, y], [x + shiftX, y + shiftY])
-        ) {
-          for (let piece of finalPieces) {
-            moves.push(
-              this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
-                c: pawnColor,
-                p: piece
-              })
-            );
-          }
-        }
       }
     }
 
diff --git a/client/src/variants/Recycle.js b/client/src/variants/Recycle.js
new file mode 100644
index 00000000..d99228cd
--- /dev/null
+++ b/client/src/variants/Recycle.js
@@ -0,0 +1,278 @@
+import { ChessRules, PiPo, Move } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+
+export const VariantRules = class RecycleRules extends ChessRules {
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const fenParsed = V.ParseFen(fen);
+    // 5) Check reserves
+    if (!fenParsed.reserve || !fenParsed.reserve.match(/^[0-9]{10,10}$/))
+      return false;
+    return true;
+  }
+
+  static ParseFen(fen) {
+    const fenParts = fen.split(" ");
+    return Object.assign(ChessRules.ParseFen(fen), {
+      reserve: fenParts[5],
+    });
+  }
+
+  static GenRandInitFen() {
+    return ChessRules.GenRandInitFen() + " 0000000000";
+  }
+
+  getFen() {
+    return (
+      super.getFen() + " " + this.getReserveFen()
+    );
+  }
+
+  getReserveFen() {
+    let counts = new Array(10);
+    for (
+      let i = 0;
+      i < V.PIECES.length - 1;
+      i++ //-1: no king reserve
+    ) {
+      counts[i] = this.reserve["w"][V.PIECES[i]];
+      counts[5 + i] = this.reserve["b"][V.PIECES[i]];
+    }
+    return counts.join("");
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    const fenParsed = V.ParseFen(fen);
+    // Also init reserves (used by the interface to show landable pieces)
+    this.reserve = {
+      w: {
+        [V.PAWN]: parseInt(fenParsed.reserve[0]),
+        [V.ROOK]: parseInt(fenParsed.reserve[1]),
+        [V.KNIGHT]: parseInt(fenParsed.reserve[2]),
+        [V.BISHOP]: parseInt(fenParsed.reserve[3]),
+        [V.QUEEN]: parseInt(fenParsed.reserve[4])
+      },
+      b: {
+        [V.PAWN]: parseInt(fenParsed.reserve[5]),
+        [V.ROOK]: parseInt(fenParsed.reserve[6]),
+        [V.KNIGHT]: parseInt(fenParsed.reserve[7]),
+        [V.BISHOP]: parseInt(fenParsed.reserve[8]),
+        [V.QUEEN]: parseInt(fenParsed.reserve[9])
+      }
+    };
+  }
+
+  getColor(i, j) {
+    if (i >= V.size.x) return i == V.size.x ? "w" : "b";
+    return this.board[i][j].charAt(0);
+  }
+
+  getPiece(i, j) {
+    if (i >= V.size.x) return V.RESERVE_PIECES[j];
+    return this.board[i][j].charAt(1);
+  }
+
+  // Used by the interface:
+  getReservePpath(color, index) {
+    return color + V.RESERVE_PIECES[index];
+  }
+
+  // Ordering on reserve pieces
+  static get RESERVE_PIECES() {
+    return [V.PAWN, V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN];
+  }
+
+  getReserveMoves([x, y]) {
+    const color = this.turn;
+    const p = V.RESERVE_PIECES[y];
+    if (this.reserve[color][p] == 0) return [];
+    let moves = [];
+    const pawnShift = p == V.PAWN ? 1 : 0;
+    for (let i = pawnShift; i < V.size.x - pawnShift; i++) {
+      for (let j = 0; j < V.size.y; j++) {
+        if (this.board[i][j] == V.EMPTY) {
+          let mv = new Move({
+            appear: [
+              new PiPo({
+                x: i,
+                y: j,
+                c: color,
+                p: p
+              })
+            ],
+            vanish: [],
+            start: { x: x, y: y }, //a bit artificial...
+            end: { x: i, y: j }
+          });
+          moves.push(mv);
+        }
+      }
+    }
+    return moves;
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    if (x >= V.size.x) {
+      // Reserves, outside of board: x == sizeX(+1)
+      return this.getReserveMoves([x, y]);
+    }
+    // Standard moves
+    return super.getPotentialMovesFrom([x, y]);
+  }
+
+  getPotentialPawnMoves([x, y]) {
+    const color = this.turn;
+    let moves = [];
+    const [sizeX, sizeY] = [V.size.x, V.size.y];
+    const shiftX = color == "w" ? -1 : 1;
+    const startRank = color == "w" ? sizeX - 2 : 1;
+    const lastRank = color == "w" ? 0 : sizeX - 1;
+
+    // One square forward
+    if (this.board[x + shiftX][y] == V.EMPTY) {
+      moves.push(
+        this.getBasicMove([x, y], [x + shiftX, y])
+      );
+      // Next condition because pawns on 1st rank can generally jump
+      if (
+        x == startRank &&
+        this.board[x + 2 * shiftX][y] == V.EMPTY
+      ) {
+        // Two squares jump
+        moves.push(this.getBasicMove([x, y], [x + 2 * shiftX, y]));
+      }
+    }
+    // Captures
+    for (let shiftY of [-1, 1]) {
+      if (
+        y + shiftY >= 0 &&
+        y + shiftY < sizeY &&
+        this.board[x + shiftX][y + shiftY] != V.EMPTY &&
+        this.canTake([x, y], [x + shiftX, y + shiftY])
+      ) {
+        moves.push(
+          this.getBasicMove([x, y], [x + shiftX, y + shiftY])
+        );
+      }
+    }
+
+    // En passant
+    const Lep = this.epSquares.length;
+    const epSquare = this.epSquares[Lep - 1]; //always at least one element
+    if (
+      !!epSquare &&
+      epSquare.x == x + shiftX &&
+      Math.abs(epSquare.y - y) == 1
+    ) {
+      let enpassantMove = this.getBasicMove([x, y], [epSquare.x, epSquare.y]);
+      enpassantMove.vanish.push({
+        x: x,
+        y: epSquare.y,
+        p: "p",
+        c: this.getColor(x, epSquare.y)
+      });
+      moves.push(enpassantMove);
+    }
+
+    // Post-processing: remove falling pawns
+    if (x + shiftX == lastRank) {
+      moves.forEach(m => {
+        m.appear.pop();
+      });
+    }
+
+    return moves;
+  }
+
+  getAllValidMoves() {
+    let moves = super.getAllValidMoves();
+    const color = this.turn;
+    for (let i = 0; i < V.RESERVE_PIECES.length; i++)
+      moves = moves.concat(
+        this.getReserveMoves([V.size.x + (color == "w" ? 0 : 1), i])
+      );
+    return this.filterValid(moves);
+  }
+
+  atLeastOneMove() {
+    if (!super.atLeastOneMove()) {
+      // Search one reserve move
+      for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
+        let moves = this.filterValid(
+          this.getReserveMoves([V.size.x + (this.turn == "w" ? 0 : 1), i])
+        );
+        if (moves.length > 0) return true;
+      }
+      return false;
+    }
+    return true;
+  }
+
+  canTake([x1, y1], [x2, y2]) {
+    // Self-captures allowed, except for the king:
+    return this.getPiece(x2, y2) != V.KING;
+  }
+
+  updateVariables(move) {
+    super.updateVariables(move);
+    if (move.vanish.length == 2 && move.appear.length == 2) return; //skip castle
+    const color = V.GetOppCol(this.turn);
+    if (move.vanish.length == 0) {
+      this.reserve[color][move.appear[0].p]--;
+      return;
+    }
+    else if (move.vanish.length == 2 && move.vanish[1].c == color) {
+      // Self-capture
+      this.reserve[color][move.vanish[1].p]++;
+    }
+  }
+
+  unupdateVariables(move) {
+    super.unupdateVariables(move);
+    if (move.vanish.length == 2 && move.appear.length == 2) return;
+    const color = this.turn;
+    if (move.vanish.length == 0) {
+      this.reserve[color][move.appear[0].p]++;
+      return;
+    }
+    else if (move.vanish.length == 2 && move.vanish[1].c == color) {
+      this.reserve[color][move.vanish[1].p]--;
+    }
+  }
+
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
+  evalPosition() {
+    let evaluation = super.evalPosition();
+    // Add reserves:
+    for (let i = 0; i < V.RESERVE_PIECES.length; i++) {
+      const p = V.RESERVE_PIECES[i];
+      evaluation += this.reserve["w"][p] * V.VALUES[p];
+      evaluation -= this.reserve["b"][p] * V.VALUES[p];
+    }
+    return evaluation;
+  }
+
+  getNotation(move) {
+    const finalSquare = V.CoordsToSquare(move.end);
+    if (move.vanish.length > 0) {
+      if (move.appear.length > 0) {
+        // Standard move
+        return super.getNotation(move);
+      } else {
+        // Pawn fallen: capturing or not
+        let res = "";
+        if (move.vanish.length == 2)
+          res += V.CoordToColumn(move.start.y) + "x";
+        return res + finalSquare;
+      }
+    }
+    // Rebirth:
+    const piece =
+      move.appear[0].p != V.PAWN ? move.appear[0].p.toUpperCase() : "";
+    return piece + "@" + V.CoordsToSquare(move.end);
+  }
+};
diff --git a/server/db/populate.sql b/server/db/populate.sql
index a9386d6a..6a847d28 100644
--- a/server/db/populate.sql
+++ b/server/db/populate.sql
@@ -18,6 +18,7 @@ insert or ignore into Variants (name,description) values
   ('Losers', 'Lose all pieces'),
   ('Magnetic', 'Laws of attraction'),
   ('Marseille', 'Move twice'),
+  ('Recycle', 'Reuse pieces'),
   ('Upsidedown', 'Board upside down'),
   ('Wildebeest', 'Balanced sliders & leapers'),
   ('Zen', 'Reverse captures');
-- 
2.44.0