From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 25 Feb 2020 23:24:14 +0000 (+0100)
Subject: Draft 2 new variants. Still 4 to add in this series
X-Git-Url: https://git.auder.net/doc/%7B%7B%20asset%28%27mixstore/current/img/common.css?a=commitdiff_plain;h=c3a86f018aba40e3926e3672c7cea87acc6d1e25;p=vchess.git

Draft 2 new variants. Still 4 to add in this series
---

diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index 56b5bab3..3c7cd7e6 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -126,6 +126,7 @@ export const translations = {
   "Your message": "Your message",
 
   // Variants boxes:
+  "Ancient rules": "Ancient rules",
   "Attract opposite king": "Attract opposite king",
   "Balanced sliders & leapers": "Balanced sliders & leapers",
   "Big board": "Big board",
@@ -136,15 +137,17 @@ export const translations = {
   "Captures reborn": "Captures reborn",
   "Change colors": "Change colors",
   "Dangerous collisions": "Dangerous collisions",
-  "Exchange pieces positions": "Exchange pieces positions",
   "Exotic captures": "Exotic captures",
   "Explosive captures": "Explosive captures",
   "In the shadow": "In the shadow",
+  "Give three checks": "Give three checks",
   "Keep antiking in check": "Keep antiking in check",
   "King crosses the board": "King crosses the board",
   "Laws of attraction": "Laws of attraction",
   "Lose all pieces": "Lose all pieces",
   "Mate any piece": "Mate any piece",
+  "Middle battle": "Middle battle",
+  "Move like a knight": "Move like a knight",
   "Move twice": "Move twice",
   "Neverending rows": "Neverending rows",
   "Pawns move diagonally": "Pawns move diagonally",
@@ -152,6 +155,8 @@ export const translations = {
   "Reverse captures": "Reverse captures",
   "Run forward": "Run forward",
   "Shared pieces": "Shared pieces",
+  "Shoot pieces": "Shoot pieces",
+  "Squares disappear": "Squares disappear",
   "Standard rules": "Standard rules",
   "Unidentified pieces": "Unidentified pieces"
 };
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index 48eb21ec..1024f787 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -126,6 +126,7 @@ export const translations = {
   "Your message": "Tu mensaje",
 
   // Variants boxes:
+  "Ancient rules": "Viejas reglas",
   "Attract opposite king": "Atraer al rey contrario",
   "Balanced sliders & leapers": "Modos de desplazamiento equilibrados",
   "Big board": "Gran tablero",
@@ -136,15 +137,17 @@ export const translations = {
   "Captures reborn": "Las capturas renacen",
   "Change colors": "Cambiar colores",
   "Dangerous collisions": "Colisiones peligrosas",
-  "Exchange pieces positions": "Intercambiar las posiciones de las piezas",
   "Exotic captures": "Capturas exóticas",
   "Explosive captures": "Capturas explosivas",
   "In the shadow": "En la sombra",
+  "Give three checks": "Dar tres jaques",
   "Keep antiking in check": "Mantener el antirey en jaque",
   "King crosses the board": "El rey cruza el tablero",
   "Laws of attraction": "Las leyes de las atracciones",
   "Lose all pieces": "Perder todas las piezas",
   "Mate any piece": "Matar cualquier pieza",
+  "Middle battle": "Batalla media",
+  "Move like a knight": "Moverse como un caballo",
   "Move twice": "Mover dos veces",
   "Neverending rows": "Filas interminables",
   "Pawns move diagonally": "Peones se mueven en diagonal",
@@ -152,6 +155,8 @@ export const translations = {
   "Reverse captures": "Capturas invertidas",
   "Run forward": "Correr hacia adelante",
   "Shared pieces": "Piezas compartidas",
+  "Shoot pieces": "Tirar de las piezas",
+  "Squares disappear": "Las casillas desaparecen",
   "Standard rules": "Reglas estandar",
   "Unidentified pieces": "Piezas no identificadas"
 };
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 1e73e1e4..592e9a06 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -126,6 +126,7 @@ export const translations = {
   "Your message": "Votre message",
 
   // Variants boxes:
+  "Ancient rules": "Règles anciennes",
   "Attract opposite king": "Attirer le roi adverse",
   "Balanced sliders & leapers": "Modes de déplacement équilibrés",
   "Big board": "Grand échiquier",
@@ -136,15 +137,17 @@ export const translations = {
   "Captures reborn": "Les captures renaissent",
   "Change colors": "Changer les couleurs",
   "Dangerous collisions": "Collisions dangeureuses",
-  "Exchange pieces positions": "Échangez les positions des pièces",
   "Exotic captures": "Captures exotiques",
   "Explosive captures": "Captures explosives",
   "In the shadow": "Dans l'ombre",
+  "Give three checks": "Donnez trois échecs",
   "Keep antiking in check": "Gardez l'antiroi en échec",
   "King crosses the board": "Le roi traverse l'échiquier",
   "Laws of attraction": "Les lois de l'attraction",
   "Lose all pieces": "Perdez toutes les pièces",
   "Mate any piece": "Mater n'importe quelle pièce",
+  "Middle battle": "Bataille du milieu",
+  "Move like a knight": "Bougez comme un cavalier",
   "Move twice": "Jouer deux coups",
   "Neverending rows": "Rangées sans fin",
   "Pawns move diagonally": "Les pions vont en diagonale",
@@ -152,6 +155,8 @@ export const translations = {
   "Reverse captures": "Captures inversées",
   "Run forward": "Courir vers l'avant",
   "Shared pieces": "Pièces partagées",
+  "Shoot pieces": "Tirez sur les pièces",
+  "Squares disappear": "Les cases disparaissent",
   "Standard rules": "Règles usuelles",
   "Unidentified pieces": "Pièces non identifiées"
 };
diff --git a/client/src/translations/rules/Arena/en.pug b/client/src/translations/rules/Arena/en.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Arena/en.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Arena/es.pug b/client/src/translations/rules/Arena/es.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Arena/es.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Arena/fr.pug b/client/src/translations/rules/Arena/fr.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Arena/fr.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Chaturanga/en.pug b/client/src/translations/rules/Chaturanga/en.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Chaturanga/en.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Chaturanga/es.pug b/client/src/translations/rules/Chaturanga/es.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Chaturanga/es.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Chaturanga/fr.pug b/client/src/translations/rules/Chaturanga/fr.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Chaturanga/fr.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Check3/en.pug b/client/src/translations/rules/Check3/en.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Check3/en.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Check3/es.pug b/client/src/translations/rules/Check3/es.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Check3/es.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Check3/fr.pug b/client/src/translations/rules/Check3/fr.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Check3/fr.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Knightrelay/en.pug b/client/src/translations/rules/Knightrelay/en.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Knightrelay/en.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Knightrelay/es.pug b/client/src/translations/rules/Knightrelay/es.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Knightrelay/es.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Knightrelay/fr.pug b/client/src/translations/rules/Knightrelay/fr.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Knightrelay/fr.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Rifle/en.pug b/client/src/translations/rules/Rifle/en.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Rifle/en.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Rifle/es.pug b/client/src/translations/rules/Rifle/es.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Rifle/es.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Rifle/fr.pug b/client/src/translations/rules/Rifle/fr.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Rifle/fr.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Wormhole/en.pug b/client/src/translations/rules/Wormhole/en.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Wormhole/en.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Wormhole/es.pug b/client/src/translations/rules/Wormhole/es.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Wormhole/es.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/translations/rules/Wormhole/fr.pug b/client/src/translations/rules/Wormhole/fr.pug
new file mode 100644
index 00000000..4f56997b
--- /dev/null
+++ b/client/src/translations/rules/Wormhole/fr.pug
@@ -0,0 +1 @@
+p TODO
diff --git a/client/src/variants/Arena.js b/client/src/variants/Arena.js
new file mode 100644
index 00000000..b92be1ee
--- /dev/null
+++ b/client/src/variants/Arena.js
@@ -0,0 +1,152 @@
+import { ChessRules } from "@/base_rules";
+
+export const VariantRules = class ArenaRules extends ChessRules {
+  static get hasFlags() {
+    return false;
+  }
+
+  static GenRandInitFen() {
+    return ChessRules.GenRandInitFen().replace("w 1111 -", "w -");
+  }
+
+  static InArena(x) {
+    return Math.abs(3.5 - x) <= 1.5;
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    const moves = super.getPotentialMovesFrom([x, y]);
+    // Eliminate moves which neither enter the arena or capture something
+    return moves.filter(m => {
+      const startInArena = V.InArena(m.start.x);
+      const endInArena = V.InArena(m.end.x);
+      return (
+        (startInArena && endInArena && m.vanish.length == 2) ||
+        (!startInArena && endInArena)
+      );
+    });
+
+    return moves;
+  }
+
+  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;
+
+    if (this.board[x + shiftX][y] == V.EMPTY) {
+      // One square forward
+      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);
+    }
+
+    return moves;
+  }
+
+  getPotentialQueenMoves(sq) {
+    return this.getSlideNJumpMoves(
+      sq,
+      V.steps[V.ROOK].concat(V.steps[V.BISHOP])
+    ).filter(m => {
+      // Filter out moves longer than 3 squares
+      return Math.max(
+        Math.abs(m.end.x - m.start.x),
+        Math.abs(m.end.y - m.start.y)) <= 3;
+    });
+  }
+
+  getPotentialKingMoves(sq) {
+    return this.getSlideNJumpMoves(
+      sq,
+      V.steps[V.ROOK].concat(V.steps[V.BISHOP])
+    ).filter(m => {
+      // Filter out moves longer than 3 squares
+      return Math.max(
+        Math.abs(m.end.x - m.start.x),
+        Math.abs(m.end.y - m.start.y)) <= 3;
+    });
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+
+  filterValid(moves) {
+    // No check conditions
+    return moves;
+  }
+
+  getCurrentScore() {
+    const color = this.turn;
+    if (!this.atLeastOneMove())
+      // I cannot move anymore
+      return color == "w" ? "0-1" : "1-0";
+    // Win if the opponent has no more pieces left (in the Arena),
+    // (and/)or if he lost both his dukes.
+    let someUnitRemain = false;
+    let atLeastOneDuke = false;
+    let somethingInArena = false;
+    outerLoop: for (let i=0; i<V.size.x; i++) {
+      for (let j=0; j<V.size.y; j++) {
+        if (this.getColor(i,j) == color) {
+          someUnitRemain = true;
+          if (this.movesCount >= 2 && V.InArena(i)) {
+            somethingInArena = true;
+            if (atLeastOneDuke)
+              break outerLoop;
+          }
+          if ([V.QUEEN,V.KING].includes(this.getPiece(i,j))) {
+            atLeastOneDuke = true;
+            if (this.movesCount < 2 || somethingInArena)
+              break outerLoop;
+          }
+        }
+      }
+    }
+    if (
+      !someUnitRemain ||
+      !atLeastOneDuke ||
+      (this.movesCount >= 2 && !somethingInArena)
+    ) {
+      return color == "w" ? "0-1" : "1-0";
+    }
+    return "*";
+  }
+};
diff --git a/client/src/variants/Chaturanga.js b/client/src/variants/Chaturanga.js
new file mode 100644
index 00000000..a1b62284
--- /dev/null
+++ b/client/src/variants/Chaturanga.js
@@ -0,0 +1,121 @@
+import { ChessRules } from "@/base_rules";
+
+export const VariantRules = class ChaturangaRules extends ChessRules {
+  static get HasFlags() {
+    return false;
+  }
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  static get ElephantSteps() {
+    return [
+      [-2, -2],
+      [-2, 2],
+      [2, -2],
+      [2, 2]
+    ];
+  }
+
+  static GenRandInitFen() {
+    return ChessRules.GenRandInitFen().replace("w 1111 -", "w");
+  }
+
+  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;
+    // Promotion in minister (queen) only:
+    const finalPiece = x + shiftX == lastRank ? V.QUEEN : V.PAWN;
+
+    if (this.board[x + shiftX][y] == V.EMPTY) {
+      // One square forward
+      moves.push(
+        this.getBasicMove([x, y], [x + shiftX, y], {
+          c: color,
+          p: finalPiece
+        })
+      );
+    }
+    // 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], {
+            c: color,
+            p: finalPiece
+          })
+        );
+      }
+    }
+
+    return moves;
+  }
+
+  getPotentialBishopMoves(sq) {
+    let moves = this.getSlideNJumpMoves(sq, V.ElephantSteps, "oneStep");
+    // Complete with "repositioning moves": like a queen, without capture
+    let repositioningMoves = this.getSlideNJumpMoves(
+      sq,
+      V.steps[V.BISHOP],
+      "oneStep"
+    ).filter(m => m.vanish.length == 1);
+    return moves.concat(repositioningMoves);
+  }
+
+  getPotentialQueenMoves(sq) {
+    return this.getSlideNJumpMoves(
+      sq,
+      V.steps[V.BISHOP],
+      "oneStep"
+    );
+  }
+
+  getPotentialKingMoves(sq) {
+    return this.getSlideNJumpMoves(
+      sq,
+      V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
+      "oneStep"
+    );
+  }
+
+  isAttackedByBishop(sq, colors) {
+    return this.isAttackedBySlideNJump(
+      sq,
+      colors,
+      V.BISHOP,
+      V.ElephantSteps,
+      "oneStep"
+    );
+  }
+
+  isAttackedByQueen(sq, colors) {
+    return this.isAttackedBySlideNJump(
+      sq,
+      colors,
+      V.QUEEN,
+      V.steps[V.BISHOP],
+      "oneStep"
+    );
+  }
+
+  static get VALUES() {
+    return {
+      p: 1,
+      r: 5,
+      n: 3,
+      b: 2.5,
+      q: 2,
+      k: 1000
+    };
+  }
+};
diff --git a/client/src/variants/Royalrace.js b/client/src/variants/Royalrace.js
index e3ed224b..6f64d24f 100644
--- a/client/src/variants/Royalrace.js
+++ b/client/src/variants/Royalrace.js
@@ -178,7 +178,10 @@ export const VariantRules = class RoyalraceRules extends ChessRules {
     if (this.kingPos[color][0] == 0)
       // The opposing edge is reached!
       return color == "w" ? "1-0" : "0-1";
-    return "*";
+    if (this.atLeastOneMove())
+      return "*";
+    // Stalemate (will probably never happen)
+    return "1/2";
   }
 
   static get SEARCH_DEPTH() {
diff --git a/client/src/variants/Wormhole.js b/client/src/variants/Wormhole.js
new file mode 100644
index 00000000..964c5e40
--- /dev/null
+++ b/client/src/variants/Wormhole.js
@@ -0,0 +1,333 @@
+import { ChessRules, PiPo, Move } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { randInt } from "@/utils/alea";
+
+// TODO:
+
+export const VariantRules = class HiddenRules extends ChessRules {
+  static get HasFlags() {
+    return false;
+  }
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  // Analyse in Hidden mode makes no sense
+  static get CanAnalyze() {
+    return false;
+  }
+
+  // Moves are revealed only when game ends
+  static get ShowMoves() {
+    return "none";
+  }
+
+  static get HIDDEN_DECODE() {
+    return {
+      s: "p",
+      t: "q",
+      u: "r",
+      c: "b",
+      o: "n",
+      l: "k"
+    };
+  }
+  static get HIDDEN_CODE() {
+    return {
+      p: "s",
+      q: "t",
+      r: "u",
+      b: "c",
+      n: "o",
+      k: "l"
+    };
+  }
+
+  // Turn a hidden piece or revealed piece into revealed piece:
+  static Decode(p) {
+    if (Object.keys(V.HIDDEN_DECODE).includes(p))
+      return V.HIDDEN_DECODE[p];
+    return p;
+  }
+
+  static get PIECES() {
+    return ChessRules.PIECES.concat(Object.values(V.HIDDEN_CODE));
+  }
+
+  // Pieces can be hidden :)
+  getPiece(i, j) {
+    const piece = this.board[i][j].charAt(1);
+    if (Object.keys(V.HIDDEN_DECODE).includes(piece))
+      return V.HIDDEN_DECODE[piece];
+    return piece;
+  }
+
+  // Scan board for kings positions (no castling)
+  scanKingsRooks(fen) {
+    this.kingPos = { w: [-1, -1], b: [-1, -1] };
+    const fenRows = V.ParseFen(fen).position.split("/");
+    for (let i = 0; i < fenRows.length; i++) {
+      let k = 0; //column index on board
+      for (let j = 0; j < fenRows[i].length; j++) {
+        switch (fenRows[i].charAt(j)) {
+          case "k":
+          case "l":
+            this.kingPos["b"] = [i, k];
+            break;
+          case "K":
+          case "L":
+            this.kingPos["w"] = [i, k];
+            break;
+          default: {
+            const num = parseInt(fenRows[i].charAt(j));
+            if (!isNaN(num)) k += num - 1;
+          }
+        }
+        k++;
+      }
+    }
+  }
+
+  getPpath(b, color, score) {
+    if (Object.keys(V.HIDDEN_DECODE).includes(b[1])) {
+      // Supposed to be hidden.
+      if (score == "*" && (!color || color != b[0]))
+        return "Hidden/" + b[0] + "p";
+      // Else: condition OK to show the piece
+      return b[0] + V.HIDDEN_DECODE[b[1]];
+    }
+    // The piece is already not supposed to be hidden:
+    return b;
+  }
+
+  getBasicMove([sx, sy], [ex, ey], tr) {
+    if (
+      tr &&
+      Object.keys(V.HIDDEN_DECODE).includes(this.board[sx][sy].charAt(1))
+    ) {
+      // The transformed piece is a priori hidden
+      tr.p = V.HIDDEN_CODE[tr.p];
+    }
+    let mv = new Move({
+      appear: [
+        new PiPo({
+          x: ex,
+          y: ey,
+          c: tr ? tr.c : this.getColor(sx, sy),
+          p: tr ? tr.p : this.board[sx][sy].charAt(1)
+        })
+      ],
+      vanish: [
+        new PiPo({
+          x: sx,
+          y: sy,
+          c: this.getColor(sx, sy),
+          p: this.board[sx][sy].charAt(1)
+        })
+      ]
+    });
+
+    // The opponent piece disappears if we take it
+    if (this.board[ex][ey] != V.EMPTY) {
+      mv.vanish.push(
+        new PiPo({
+          x: ex,
+          y: ey,
+          c: this.getColor(ex, ey),
+          p: this.board[ex][ey].charAt(1)
+        })
+      );
+      // Pieces are revealed when they capture
+      mv.appear[0].p = V.Decode(mv.appear[0].p);
+    }
+
+    return mv;
+  }
+
+  // What are the king moves from square x,y ?
+  getPotentialKingMoves(sq) {
+    // No castling:
+    return this.getSlideNJumpMoves(
+      sq,
+      V.steps[V.ROOK].concat(V.steps[V.BISHOP]),
+      "oneStep"
+    );
+  }
+
+  filterValid(moves) {
+    return moves;
+  }
+
+  static GenRandInitFen() {
+    let pieces = { w: new Array(8), b: new Array(8) };
+    // Shuffle pieces + pawns on two first ranks
+    for (let c of ["w", "b"]) {
+      let positions = ArrayFun.range(16);
+
+      // Get random squares for bishops
+      let randIndex = 2 * randInt(8);
+      const bishop1Pos = positions[randIndex];
+      // The second bishop must be on a square of different color
+      let randIndex_tmp = 2 * randInt(8) + 1;
+      const bishop2Pos = positions[randIndex_tmp];
+      // Remove chosen squares
+      positions.splice(Math.max(randIndex, randIndex_tmp), 1);
+      positions.splice(Math.min(randIndex, randIndex_tmp), 1);
+
+      // Get random squares for knights
+      randIndex = randInt(14);
+      const knight1Pos = positions[randIndex];
+      positions.splice(randIndex, 1);
+      randIndex = randInt(13);
+      const knight2Pos = positions[randIndex];
+      positions.splice(randIndex, 1);
+
+      // Get random squares for rooks
+      randIndex = randInt(12);
+      const rook1Pos = positions[randIndex];
+      positions.splice(randIndex, 1);
+      randIndex = randInt(11);
+      const rook2Pos = positions[randIndex];
+      positions.splice(randIndex, 1);
+
+      // Get random square for queen
+      randIndex = randInt(10);
+      const queenPos = positions[randIndex];
+      positions.splice(randIndex, 1);
+
+      // Get random square for king
+      randIndex = randInt(9);
+      const kingPos = positions[randIndex];
+      positions.splice(randIndex, 1);
+
+      // Pawns position are all remaining slots:
+      for (let p of positions)
+        pieces[c][p] = "s";
+
+      // Finally put the shuffled pieces in the board array
+      pieces[c][rook1Pos] = "u";
+      pieces[c][knight1Pos] = "o";
+      pieces[c][bishop1Pos] = "c";
+      pieces[c][queenPos] = "t";
+      pieces[c][kingPos] = "l";
+      pieces[c][bishop2Pos] = "c";
+      pieces[c][knight2Pos] = "o";
+      pieces[c][rook2Pos] = "u";
+    }
+    let upFen = pieces["b"].join("");
+    upFen = upFen.substr(0,8) + "/" + upFen.substr(8).split("").reverse().join("");
+    let downFen = pieces["b"].join("").toUpperCase();
+    downFen = downFen.substr(0,8) + "/" + downFen.substr(8).split("").reverse().join("");
+    return upFen + "/8/8/8/8/" + downFen + " w 0";
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+
+  updateVariables(move) {
+    super.updateVariables(move);
+    if (
+      move.vanish.length >= 2 &&
+      [V.KING,V.HIDDEN_CODE[V.KING]].includes(move.vanish[1].p)
+    ) {
+      // We took opponent king
+      this.kingPos[this.turn] = [-1, -1];
+    }
+  }
+
+  unupdateVariables(move) {
+    super.unupdateVariables(move);
+    const c = move.vanish[0].c;
+    const oppCol = V.GetOppCol(c);
+    if (this.kingPos[oppCol][0] < 0)
+      // Last move took opponent's king:
+      this.kingPos[oppCol] = [move.vanish[1].x, move.vanish[1].y];
+  }
+
+  getCurrentScore() {
+    const color = this.turn;
+    const kp = this.kingPos[color];
+    if (kp[0] < 0)
+      // King disappeared
+      return color == "w" ? "0-1" : "1-0";
+    // Assume that stalemate is impossible:
+    return "*";
+  }
+
+  getComputerMove() {
+    const color = this.turn;
+    let moves = this.getAllValidMoves();
+    for (let move of moves) {
+      move.eval = 0; //a priori...
+
+      // Can I take something ? If yes, do it with some probability
+      if (move.vanish.length == 2 && move.vanish[1].c != color) {
+        // OK this isn't a castling move
+        const myPieceVal = V.VALUES[move.appear[0].p];
+        const hisPieceVal = Object.keys(V.HIDDEN_DECODE).includes(move.vanish[1].p)
+          ? undefined
+          : V.VALUES[move.vanish[1].p];
+        if (!hisPieceVal) {
+          // Opponent's piece is unknown: do not take too much risk
+          move.eval = -myPieceVal + 1.5; //so that pawns always take
+        }
+        // Favor captures
+        else if (myPieceVal <= hisPieceVal)
+          move.eval = hisPieceVal - myPieceVal + 1;
+        else {
+          // Taking a pawn with minor piece,
+          // or minor piece or pawn with a rook,
+          // or anything but a queen with a queen,
+          // or anything with a king.
+          move.eval = hisPieceVal - myPieceVal;
+        }
+      } else {
+        // If no capture, favor small step moves,
+        // but sometimes move the knight anyway
+        const penalty = V.Decode(move.vanish[0].p) != V.KNIGHT
+          ? Math.abs(move.end.x - move.start.x) + Math.abs(move.end.y - move.start.y)
+          : (Math.random() < 0.5 ? 3 : 1);
+        move.eval -= penalty / (V.size.x + V.size.y - 1);
+      }
+
+      // TODO: also favor movements toward the center?
+    }
+
+    moves.sort((a, b) => b.eval - a.eval);
+    let candidates = [0];
+    for (let j = 1; j < moves.length && moves[j].eval == moves[0].eval; j++)
+      candidates.push(j);
+    return moves[candidates[randInt(candidates.length)]];
+  }
+
+  getNotation(move) {
+    // Translate final square
+    const finalSquare = V.CoordsToSquare(move.end);
+
+    const piece = this.getPiece(move.start.x, move.start.y);
+    if (piece == V.PAWN) {
+      // Pawn move
+      let notation = "";
+      if (move.vanish.length > move.appear.length) {
+        // Capture
+        const startColumn = V.CoordToColumn(move.start.y);
+        notation = startColumn + "x" + finalSquare;
+      }
+      else notation = finalSquare;
+      if (move.appear.length > 0 && !["p","s"].includes(move.appear[0].p)) {
+        // Promotion
+        const appearPiece = V.Decode(move.appear[0].p);
+        notation += "=" + appearPiece.toUpperCase();
+      }
+      return notation;
+    }
+    // Piece movement
+    return (
+      piece.toUpperCase() +
+      (move.vanish.length > move.appear.length ? "x" : "") +
+      finalSquare
+    );
+  }
+};
diff --git a/server/db/populate.sql b/server/db/populate.sql
index 84a34d5d..7043996f 100644
--- a/server/db/populate.sql
+++ b/server/db/populate.sql
@@ -5,11 +5,14 @@ insert or ignore into Variants (name,description) values
   ('Allmate', 'Mate any piece'),
   ('Antiking', 'Keep antiking in check'),
   ('Antimatter', 'Dangerous collisions'),
+  ('Arena', 'Middle battle'),
   ('Atomic', 'Explosive captures'),
   ('Baroque', 'Exotic captures'),
   ('Benedict', 'Change colors'),
   ('Berolina', 'Pawns move diagonally'),
+  ('Chaturanga', 'Ancient rules'),
   ('Checkered', 'Shared pieces'),
+  ('Check3', 'Give three checks'),
   ('Chess960', 'Standard rules'),
   ('Circular', 'Run forward'),
   ('Crazyhouse', 'Captures reborn'),
@@ -19,12 +22,15 @@ insert or ignore into Variants (name,description) values
   ('Extinction', 'Capture all of a kind'),
   ('Grand', 'Big board'),
   ('Hidden', 'Unidentified pieces'),
+  ('Knightrelay', 'Move like a knight'),
   ('Losers', 'Lose all pieces'),
   ('Magnetic', 'Laws of attraction'),
   ('Marseille', 'Move twice'),
+  ('Rifle', 'Shoot pieces'),
   ('Royalrace', 'King crosses the board'),
   ('Recycle', 'Reuse pieces'),
   ('Suction', 'Attract opposite king'),
   ('Upsidedown', 'Board upside down'),
   ('Wildebeest', 'Balanced sliders & leapers'),
+  ('Wormhole', 'Squares disappear'),
   ('Zen', 'Reverse captures');