From 32f6285ee325a14286562a53baefc647201df2af Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Tue, 17 Mar 2020 22:40:32 +0100
Subject: [PATCH] Generalize pawn movements: cleaner and smaller code

---
 client/src/base_rules.js                     | 173 +++++++-----
 client/src/playCompMove.js                   |   4 +-
 client/src/translations/rules/Capture/es.pug |   4 +-
 client/src/variants/Alice.js                 |   2 +-
 client/src/variants/Allmate1.js              |   8 +-
 client/src/variants/Allmate2.js              |   8 +-
 client/src/variants/Antiking1.js             |  84 +-----
 client/src/variants/Antiking2.js             |   2 +-
 client/src/variants/Antimatter.js            |   2 +-
 client/src/variants/Arena.js                 |  64 +----
 client/src/variants/Atomic.js                |   2 +-
 client/src/variants/Baroque.js               |  46 +---
 client/src/variants/Benedict.js              | 135 +---------
 client/src/variants/Berolina.js              |  42 +--
 client/src/variants/Capture.js               |   8 +-
 client/src/variants/Checkered.js             | 168 ++----------
 client/src/variants/Chess960.js              |   2 +-
 client/src/variants/Circular.js              |  58 ++--
 client/src/variants/Crazyhouse.js            |   2 +-
 client/src/variants/Cylinder.js              |   2 +-
 client/src/variants/Dark.js                  |   2 +-
 client/src/variants/Eightpieces.js           |   2 +-
 client/src/variants/Enpassant.js             |  58 +---
 client/src/variants/Extinction.js            |  47 +---
 client/src/variants/Grand.js                 |   4 +-
 client/src/variants/Grasshopper.js           |  13 +-
 client/src/variants/Hidden.js                |   2 +-
 client/src/variants/Hiddenqueen.js           |  78 +-----
 client/src/variants/Knightmate.js            |   2 +-
 client/src/variants/Knightrelay1.js          |   2 +-
 client/src/variants/Knightrelay2.js          |   2 +-
 client/src/variants/Losers.js                |   2 +-
 client/src/variants/Magnetic.js              |   2 +-
 client/src/variants/Marseille.js             |  51 +---
 client/src/variants/Racingkings.js           |   2 +-
 client/src/variants/Recycle.js               |  81 ++----
 client/src/variants/Rifle.js                 |  55 +---
 client/src/variants/Royalrace.js             |   2 +-
 client/src/variants/Shatranj.js              |  52 +---
 client/src/variants/Suction.js               |  47 +---
 client/src/variants/Suicide.js               | 100 ++-----
 client/src/variants/Threechecks.js           |   2 +-
 client/src/variants/Upsidedown.js            |   2 +-
 client/src/variants/Wildebeest.js            |   2 +-
 client/src/variants/Wormhole.js              |   2 +-
 client/src/variants/Zen.js                   |   2 +-
 client/src/views/Analyse.vue                 |   2 +-
 client/src/views/Game.vue                    | 270 ++++++++++---------
 client/src/views/Hall.vue                    |  43 +--
 client/src/views/Problems.vue                |  10 +-
 client/src/views/Rules.vue                   |   6 +-
 51 files changed, 505 insertions(+), 1258 deletions(-)

diff --git a/client/src/base_rules.js b/client/src/base_rules.js
index 29b3af60..46064758 100644
--- a/client/src/base_rules.js
+++ b/client/src/base_rules.js
@@ -42,7 +42,19 @@ export const ChessRules = class ChessRules {
     return V.HasFlags;
   }
 
-  // Some variants don't have en-passant
+  // Pawns specifications
+  static get PawnSpecs() {
+    return {
+      directions: { 'w': -1, 'b': 1 },
+      twoSquares: true,
+      promotions: [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN],
+      canCapture: true,
+      captureBackward: false,
+      bidirectional: false
+    };
+  }
+
+  // En-passant captures need a stack of squares:
   static get HasEnpassant() {
     return true;
   }
@@ -646,82 +658,115 @@ export const ChessRules = class ChessRules {
     return moves;
   }
 
+  // Special case of en-passant captures: treated separately
+  getEnpassantCaptures([x, y], shiftX) {
+    const Lep = this.epSquares.length;
+    const epSquare = this.epSquares[Lep - 1]; //always at least one element
+    let enpassantMove = null;
+    if (
+      !!epSquare &&
+      epSquare.x == x + shiftX &&
+      Math.abs(epSquare.y - y) == 1
+    ) {
+      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)
+      });
+    }
+    return !!enpassantMove ? [enpassantMove] : [];
+  }
+
   // What are the pawn moves from square x,y ?
-  getPotentialPawnMoves([x, y]) {
+  getPotentialPawnMoves([x, y], promotions) {
     const color = this.turn;
-    let moves = [];
     const [sizeX, sizeY] = [V.size.x, V.size.y];
-    const shiftX = color == "w" ? -1 : 1;
+    const pawnShiftX = V.PawnSpecs.directions[color];
     const firstRank = color == "w" ? sizeX - 1 : 0;
     const startRank = color == "w" ? sizeX - 2 : 1;
     const lastRank = color == "w" ? 0 : sizeX - 1;
 
-    // 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];
-      if (this.board[x + shiftX][y] == V.EMPTY) {
-        // One square forward
-        for (let piece of finalPieces) {
-          moves.push(
-            this.getBasicMove([x, y], [x + shiftX, y], {
-              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]));
-        }
+    // Consider all potential promotions:
+    const addMoves = ([x1, y1], [x2, y2], moves) => {
+      let finalPieces = [V.PAWN];
+      if (x2 == lastRank) {
+        // promotions arg: special override for Hiddenqueen variant
+        if (!!promotions) finalPieces = promotions;
+        else if (!!V.PawnSpecs.promotions)
+          finalPieces = V.PawnSpecs.promotions;
       }
-      // 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: color,
-                p: piece
-              })
-            );
+      for (let piece of finalPieces) {
+        moves.push(
+          this.getBasicMove([x1, y1], [x2, y2], {
+            c: color,
+            p: piece
+          })
+        );
+      }
+    }
+
+    // Pawn movements in shiftX direction:
+    const getPawnMoves = (shiftX) => {
+      let moves = [];
+      // NOTE: next condition is generally true (no pawn on last rank)
+      if (x + shiftX >= 0 && x + shiftX < sizeX) {
+        if (this.board[x + shiftX][y] == V.EMPTY) {
+          // One square forward
+          addMoves([x, y], [x + shiftX, y], moves);
+          // Next condition because pawns on 1st rank can generally jump
+          if (
+            V.PawnSpecs.twoSquares &&
+            [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
+        if (V.PawnSpecs.canCapture) {
+          for (let shiftY of [-1, 1]) {
+            if (
+              y + shiftY >= 0 &&
+              y + shiftY < sizeY
+            ) {
+              if (
+                this.board[x + shiftX][y + shiftY] != V.EMPTY &&
+                this.canTake([x, y], [x + shiftX, y + shiftY])
+              ) {
+                addMoves([x, y], [x + shiftX, y + shiftY], moves);
+              }
+              if (
+                V.PawnSpecs.captureBackward &&
+                x - shiftX >= 0 && x - shiftX < V.size.x &&
+                this.board[x - shiftX][y + shiftY] != V.EMPTY &&
+                this.canTake([x, y], [x - shiftX, y + shiftY])
+              ) {
+                addMoves([x, y], [x + shiftX, y + shiftY], moves);
+              }
+            }
           }
         }
       }
+      return moves;
     }
 
+    let pMoves = getPawnMoves(pawnShiftX);
+    if (V.PawnSpecs.bidirectional)
+      pMoves = pMoves.concat(getPawnMoves(-pawnShiftX));
+
     if (V.HasEnpassant) {
-      // 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);
-      }
+      // NOTE: backward en-passant captures are not considered
+      // because no rules define them (for now).
+      Array.prototype.push.apply(
+        pMoves,
+        this.getEnpassantCaptures([x, y], pawnShiftX)
+      );
     }
 
-    return moves;
+    return pMoves;
   }
 
   // What are the rook moves from square x,y ?
@@ -781,6 +826,11 @@ export const ChessRules = class ChessRules {
       if (this.castleFlags[c][castleSide] >= V.size.y) continue;
       // If this code is reached, rooks and king are on initial position
 
+      const rookPos = this.castleFlags[c][castleSide];
+      if (this.getColor(x, rookPos) != c)
+        // Rook is here but changed color (see Benedict)
+        continue;
+
       // Nothing on the path of the king ? (and no checks)
       const finDist = finalSquares[castleSide][0] - y;
       let step = finDist / Math.max(1, Math.abs(finDist));
@@ -800,7 +850,6 @@ export const ChessRules = class ChessRules {
 
       // Nothing on the path to the rook?
       step = castleSide == 0 ? -1 : 1;
-      const rookPos = this.castleFlags[c][castleSide];
       for (i = y + step; i != rookPos; i += step) {
         if (this.board[x][i] != V.EMPTY) continue castlingCheck;
       }
diff --git a/client/src/playCompMove.js b/client/src/playCompMove.js
index 05c2286c..d83148fb 100644
--- a/client/src/playCompMove.js
+++ b/client/src/playCompMove.js
@@ -2,8 +2,8 @@
 onmessage = async function(e) {
   switch (e.data[0]) {
     case "scripts": {
-      const vModule = await import("@/variants/" + e.data[1] + ".js");
-      self.V = vModule.VariantRules;
+      await import("@/variants/" + e.data[1] + ".js")
+      .then((vModule) => { self.V = vModule[e.data[1] + "Rules"]; });
       break;
     }
     case "init": {
diff --git a/client/src/translations/rules/Capture/es.pug b/client/src/translations/rules/Capture/es.pug
index 4994990e..505a04e7 100644
--- a/client/src/translations/rules/Capture/es.pug
+++ b/client/src/translations/rules/Capture/es.pug
@@ -1,5 +1,5 @@
 p.boxed
-  Las capturas son obligatorias. Gana por jaque mate.
+  | Las capturas son obligatorias. Gana por jaque mate.
 
 p.
   Todo va como el ajedrez ortodoxo, pero cuando es posible
@@ -11,7 +11,7 @@ p.
   como en el siguiente diagrama: Qxd7 será forzado, perdiendo a la dama.
 
 figure.diagram-container
-  .diagrama
+  .diagram
     | fen:rnbqkbnr/pppp1pp1/7p/4P3/8/8/PPP1PPPP/RNBQKBNR:
   figcaption Después de 1.d4?? e5 2.dxe5 h6.
 
diff --git a/client/src/variants/Alice.js b/client/src/variants/Alice.js
index d64c0778..d30acf05 100644
--- a/client/src/variants/Alice.js
+++ b/client/src/variants/Alice.js
@@ -3,7 +3,7 @@ import { ArrayFun } from "@/utils/array";
 
 // NOTE: alternative implementation, probably cleaner = use only 1 board
 // TODO? atLeastOneMove() would be more efficient if rewritten here (less sideBoard computations)
-export const VariantRules = class AliceRules extends ChessRules {
+export class AliceRules extends ChessRules {
   static get ALICE_PIECES() {
     return {
       s: "p",
diff --git a/client/src/variants/Allmate1.js b/client/src/variants/Allmate1.js
index bc10478e..65e20b79 100644
--- a/client/src/variants/Allmate1.js
+++ b/client/src/variants/Allmate1.js
@@ -1,6 +1,6 @@
 import { ChessRules, PiPo, Move } from "@/base_rules";
 
-export const VariantRules = class Allmate1Rules extends ChessRules {
+export class Allmate1Rules extends ChessRules {
   static get HasEnpassant() {
     return false;
   }
@@ -32,7 +32,7 @@ export const VariantRules = class Allmate1Rules extends ChessRules {
       let attacked = {};
       for (let i=0; i<V.size.x; i++) {
         for (let j=0; j<V.size.y; j++) {
-          if (this.getColor(i,j) == oppCol && this.isAttacked([i,j], [color]))
+          if (this.getColor(i,j) == oppCol && this.isAttacked([i,j], color))
             attacked[i+"_"+j] = [i,j];
         }
       }
@@ -70,7 +70,7 @@ export const VariantRules = class Allmate1Rules extends ChessRules {
                 if (om.start.x == sq[0] && om.start.y == sq[1])
                   // Piece moved:
                   sq = [om.appear[0].x, om.appear[0].y];
-                if (!this.isAttacked(sq, [color]))
+                if (!this.isAttacked(sq, color))
                   delete attacked[origSq[0]+"_"+origSq[1]];
               });
               V.UndoOnBoard(this.board, om);
@@ -222,7 +222,7 @@ export const VariantRules = class Allmate1Rules extends ChessRules {
                 if (em.start.x == attacked[0] && em.start.y == attacked[1])
                   // King moved:
                   sq = [em.appear[0].x, em.appear[0].y];
-                if (!this.isAttacked(sq, [oppCol]))
+                if (!this.isAttacked(sq, oppCol))
                   res = true;
                 V.UndoOnBoard(this.board, em);
                 if (res)
diff --git a/client/src/variants/Allmate2.js b/client/src/variants/Allmate2.js
index daa3542a..386bf697 100644
--- a/client/src/variants/Allmate2.js
+++ b/client/src/variants/Allmate2.js
@@ -1,6 +1,6 @@
 import { ChessRules, PiPo, Move } from "@/base_rules";
 
-export const VariantRules = class Allmate2Rules extends ChessRules {
+export class Allmate2Rules extends ChessRules {
   static get HasEnpassant() {
     return false;
   }
@@ -32,7 +32,7 @@ export const VariantRules = class Allmate2Rules extends ChessRules {
       let attacked = {};
       for (let i=0; i<V.size.x; i++) {
         for (let j=0; j<V.size.y; j++) {
-          if (this.getColor(i,j) == oppCol && this.isAttacked([i,j], [color]))
+          if (this.getColor(i,j) == oppCol && this.isAttacked([i,j], color))
             attacked[i+"_"+j] = [i,j];
         }
       }
@@ -74,7 +74,7 @@ export const VariantRules = class Allmate2Rules extends ChessRules {
                 if (om.start.x == sq[0] && om.start.y == sq[1])
                   // Piece moved:
                   sq = [om.appear[0].x, om.appear[0].y];
-                if (!this.isAttacked(sq, [color]))
+                if (!this.isAttacked(sq, color))
                   delete attacked[origSq[0]+"_"+origSq[1]];
               });
               V.UndoOnBoard(this.board, om);
@@ -226,7 +226,7 @@ export const VariantRules = class Allmate2Rules extends ChessRules {
                 if (em.start.x == attacked[0] && em.start.y == attacked[1])
                   // King moved:
                   sq = [em.appear[0].x, em.appear[0].y];
-                if (!this.isAttacked(sq, [oppCol]))
+                if (!this.isAttacked(sq, oppCol))
                   res = true;
                 V.UndoOnBoard(this.board, em);
                 if (res)
diff --git a/client/src/variants/Antiking1.js b/client/src/variants/Antiking1.js
index ed6173e0..75cc14cb 100644
--- a/client/src/variants/Antiking1.js
+++ b/client/src/variants/Antiking1.js
@@ -1,10 +1,15 @@
 import { ChessRules } from "@/base_rules";
+import { BerolinaRules } from "@/variants/Berolina";
 import { ArrayFun } from "@/utils/array";
 import { randInt } from "@/utils/alea";
 
-export const VariantRules = class Antiking1Rules extends ChessRules {
-  static get HasEnpassant() {
-    return false;
+export class Antiking1Rules extends BerolinaRules {
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      { twoSquares: false }
+    );
   }
 
   static get HasCastle() {
@@ -110,43 +115,6 @@ export const VariantRules = class Antiking1Rules extends ChessRules {
     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;
-    const lastRank = color == "w" ? 0 : sizeX - 1;
-    const finalPieces =
-      x + shiftX == lastRank ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN] : [V.PAWN];
-
-    // One square diagonally
-    for (let shiftY of [-1, 1]) {
-      if (this.board[x + shiftX][y + shiftY] == V.EMPTY) {
-        for (let piece of finalPieces) {
-          moves.push(
-            this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
-              c: color,
-              p: piece
-            })
-          );
-        }
-      }
-    }
-    // Capture
-    if (
-      this.board[x + shiftX][y] != V.EMPTY &&
-      this.canTake([x, y], [x + shiftX, y])
-    ) {
-      for (let piece of finalPieces)
-        moves.push(
-          this.getBasicMove([x, y], [x + shiftX, y], { c: color, p: piece })
-        );
-    }
-
-    return moves;
-  }
-
   getPotentialAntikingMoves(sq) {
     // The antiking moves like a king (only captured colors differ)
     return this.getSlideNJumpMoves(
@@ -163,19 +131,6 @@ export const VariantRules = class Antiking1Rules extends ChessRules {
     );
   }
 
-  isAttackedByPawn([x, y], color) {
-    let pawnShift = (color == "w" ? 1 : -1);
-    if (x + pawnShift >= 0 && x + pawnShift < V.size.x) {
-      if (
-        this.getPiece(x + pawnShift, y) == V.PAWN &&
-        this.getColor(x + pawnShift, y) == color
-      ) {
-        return true;
-      }
-    }
-    return false;
-  }
-
   isAttackedByKing([x, y], color) {
     // Antiking is not attacked by king:
     if (this.getPiece(x, y) == V.ANTIKING) return false;
@@ -267,27 +222,4 @@ export const VariantRules = class Antiking1Rules extends ChessRules {
   static get SEARCH_DEPTH() {
     return 2;
   }
-
-  // TODO: notation copied from Berolina
-  getNotation(move) {
-    const piece = this.getPiece(move.start.x, move.start.y);
-    if (piece == V.PAWN) {
-      // Pawn move
-      const finalSquare = V.CoordsToSquare(move.end);
-      let notation = "";
-      if (move.vanish.length == 2)
-        //capture
-        notation = "Px" + finalSquare;
-      else {
-        // No capture: indicate the initial square for potential ambiguity
-        const startSquare = V.CoordsToSquare(move.start);
-        notation = startSquare + finalSquare;
-      }
-      if (move.appear[0].p != V.PAWN)
-        // Promotion
-        notation += "=" + move.appear[0].p.toUpperCase();
-      return notation;
-    }
-    return super.getNotation(move); //all other pieces are orthodox
-  }
 };
diff --git a/client/src/variants/Antiking2.js b/client/src/variants/Antiking2.js
index 3be41af5..0a427430 100644
--- a/client/src/variants/Antiking2.js
+++ b/client/src/variants/Antiking2.js
@@ -2,7 +2,7 @@ import { ChessRules } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
 import { randInt } from "@/utils/alea";
 
-export const VariantRules = class Antiking2Rules extends ChessRules {
+export class Antiking2Rules extends ChessRules {
   static get ANTIKING() {
     return "a";
   }
diff --git a/client/src/variants/Antimatter.js b/client/src/variants/Antimatter.js
index 63d5ae56..f8f4c73e 100644
--- a/client/src/variants/Antimatter.js
+++ b/client/src/variants/Antimatter.js
@@ -1,6 +1,6 @@
 import { ChessRules } from "@/base_rules";
 
-export const VariantRules = class AntimatterRules extends ChessRules {
+export class AntimatterRules extends ChessRules {
   getPotentialMovesFrom([x, y]) {
     let moves = super.getPotentialMovesFrom([x, y]);
 
diff --git a/client/src/variants/Arena.js b/client/src/variants/Arena.js
index 620b980a..b2b18ee6 100644
--- a/client/src/variants/Arena.js
+++ b/client/src/variants/Arena.js
@@ -1,10 +1,18 @@
 import { ChessRules } from "@/base_rules";
 
-export const VariantRules = class ArenaRules extends ChessRules {
+export class ArenaRules extends ChessRules {
   static get HasFlags() {
     return false;
   }
 
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      { captureBackward: true }
+    );
+  }
+
   static GenRandInitFen(randomness) {
     return ChessRules.GenRandInitFen(randomness).slice(0, -6) + "-";
   }
@@ -28,60 +36,6 @@ export const VariantRules = class ArenaRules extends ChessRules {
     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: also possible backward
-    for (let shiftY of [-1, 1]) {
-      if (y + shiftY >= 0 && y + shiftY < sizeY) {
-        for (let direction of [-1,1]) {
-          if (
-            this.board[x + direction][y + shiftY] != V.EMPTY &&
-            this.canTake([x, y], [x + direction, y + shiftY])
-          ) {
-            moves.push(this.getBasicMove([x, y], [x + direction, 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,
diff --git a/client/src/variants/Atomic.js b/client/src/variants/Atomic.js
index 19521c2d..a6a5625b 100644
--- a/client/src/variants/Atomic.js
+++ b/client/src/variants/Atomic.js
@@ -1,6 +1,6 @@
 import { ChessRules, PiPo } from "@/base_rules";
 
-export const VariantRules = class AtomicRules extends ChessRules {
+export class AtomicRules extends ChessRules {
   getEpSquare(moveOrSquare) {
     if (typeof moveOrSquare !== "object" || moveOrSquare.appear.length > 0)
       return super.getEpSquare(moveOrSquare);
diff --git a/client/src/variants/Baroque.js b/client/src/variants/Baroque.js
index a19d09e5..92bae77f 100644
--- a/client/src/variants/Baroque.js
+++ b/client/src/variants/Baroque.js
@@ -1,8 +1,8 @@
 import { ChessRules, PiPo, Move } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
-import { randInt } from "@/utils/alea";
+import { shuffle } from "@/utils/alea";
 
-export const VariantRules = class BaroqueRules extends ChessRules {
+export class BaroqueRules extends ChessRules {
   static get HasFlags() {
     return false;
   }
@@ -558,46 +558,10 @@ export const VariantRules = class BaroqueRules extends ChessRules {
         break;
       }
 
-      let positions = ArrayFun.range(8);
       // Get random squares for every piece, totally freely
-
-      let randIndex = randInt(8);
-      const bishop1Pos = positions[randIndex];
-      positions.splice(randIndex, 1);
-
-      randIndex = randInt(7);
-      const bishop2Pos = positions[randIndex];
-      positions.splice(randIndex, 1);
-
-      randIndex = randInt(6);
-      const knight1Pos = positions[randIndex];
-      positions.splice(randIndex, 1);
-
-      randIndex = randInt(5);
-      const knight2Pos = positions[randIndex];
-      positions.splice(randIndex, 1);
-
-      randIndex = randInt(4);
-      const queenPos = positions[randIndex];
-      positions.splice(randIndex, 1);
-
-      randIndex = randInt(3);
-      const kingPos = positions[randIndex];
-      positions.splice(randIndex, 1);
-
-      randIndex = randInt(2);
-      const rookPos = positions[randIndex];
-      positions.splice(randIndex, 1);
-      const immobilizerPos = positions[0];
-
-      pieces[c][bishop1Pos] = "b";
-      pieces[c][bishop2Pos] = "b";
-      pieces[c][knight1Pos] = "n";
-      pieces[c][knight2Pos] = "n";
-      pieces[c][queenPos] = "q";
-      pieces[c][kingPos] = "k";
-      pieces[c][rookPos] = "r";
-      pieces[c][immobilizerPos] = "m";
+      let positions = shuffle(ArrayFun.range(8));
+      const composition = ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'm'];
+      for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
     }
     return (
       pieces["b"].join("") +
diff --git a/client/src/variants/Benedict.js b/client/src/variants/Benedict.js
index f0d4f2ba..fbc4b488 100644
--- a/client/src/variants/Benedict.js
+++ b/client/src/variants/Benedict.js
@@ -1,10 +1,18 @@
 import { ChessRules, PiPo, Move } from "@/base_rules";
 
-export const VariantRules = class BenedictRules extends ChessRules {
+export class BenedictRules extends ChessRules {
   static get HasEnpassant() {
     return false;
   }
 
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      { canCapture: false }
+    );
+  }
+
   // TODO(?): some duplicated code in 2 next functions
   getSlideNJumpMoves([x, y], steps, oneStep) {
     let moves = [];
@@ -64,126 +72,6 @@ export const VariantRules = class BenedictRules extends ChessRules {
     return squares;
   }
 
-  getPotentialPawnMoves([x, y]) {
-    const color = this.getColor(x, y);
-    let moves = [];
-    const sizeY = V.size.y;
-    const shift = color == "w" ? -1 : 1;
-    const startRank = color == "w" ? sizeY - 2 : 1;
-    const firstRank = color == "w" ? sizeY - 1 : 0;
-    const lastRank = color == "w" ? 0 : sizeY - 1;
-
-    if (x + shift != lastRank) {
-      // Normal moves
-      if (this.board[x + shift][y] == V.EMPTY) {
-        moves.push(this.getBasicMove([x, y], [x + shift, y]));
-        if (
-          [startRank, firstRank].includes(x) &&
-          this.board[x + 2 * shift][y] == V.EMPTY
-        ) {
-          // Two squares jump
-          moves.push(this.getBasicMove([x, y], [x + 2 * shift, y]));
-        }
-      }
-    }
-    else {
-      // Promotion
-      let promotionPieces = [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN];
-      promotionPieces.forEach(p => {
-        // Normal move
-        if (this.board[x + shift][y] == V.EMPTY)
-          moves.push(
-            this.getBasicMove([x, y], [x + shift, y], { c: color, p: p })
-          );
-      });
-    }
-
-    // No en passant here
-
-    return moves;
-  }
-
-  // No "under check" verifications:
-  getCastleMoves([x, y]) {
-    const c = this.getColor(x, y);
-    if (x != (c == "w" ? V.size.x - 1 : 0) || y != this.INIT_COL_KING[c])
-      return []; //x isn't first rank, or king has moved (shortcut)
-
-    // Castling ?
-    const oppCol = V.GetOppCol(c);
-    let moves = [];
-    let i = 0;
-    // King, then rook:
-    const finalSquares = [
-      [2, 3],
-      [V.size.y - 2, V.size.y - 3]
-    ];
-    castlingCheck: for (
-      let castleSide = 0;
-      castleSide < 2;
-      castleSide++ //large, then small
-    ) {
-      if (this.castleFlags[c][castleSide] >= 8) continue;
-      // If this code is reached, rooks and king are on initial position
-
-      const rookPos = this.castleFlags[c][castleSide];
-      if (this.getColor(x, rookPos) != c)
-        // Rook is here but changed color
-        continue;
-
-      // Nothing on the path of the king ?
-      const finDist = finalSquares[castleSide][0] - y;
-      let step = finDist / Math.max(1, Math.abs(finDist));
-      for (let i = y; i != finalSquares[castleSide][0]; i += step) {
-        if (
-          this.board[x][i] != V.EMPTY &&
-            // NOTE: next check is enough, because of chessboard constraints
-            (this.getColor(x, i) != c ||
-              ![V.KING, V.ROOK].includes(this.getPiece(x, i)))
-        ) {
-          continue castlingCheck;
-        }
-      }
-
-      // Nothing on the path to the rook?
-      step = castleSide == 0 ? -1 : 1;
-      for (i = y + step; i != rookPos; i += step) {
-        if (this.board[x][i] != V.EMPTY) continue castlingCheck;
-      }
-
-      // Nothing on final squares, except maybe king and castling rook?
-      for (i = 0; i < 2; i++) {
-        if (
-          this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
-          this.getPiece(x, finalSquares[castleSide][i]) != V.KING &&
-          finalSquares[castleSide][i] != rookPos
-        ) {
-          continue castlingCheck;
-        }
-      }
-
-      // If this code is reached, castle is valid
-      moves.push(
-        new Move({
-          appear: [
-            new PiPo({ x: x, y: finalSquares[castleSide][0], p: V.KING, c: c }),
-            new PiPo({ x: x, y: finalSquares[castleSide][1], p: V.ROOK, c: c })
-          ],
-          vanish: [
-            new PiPo({ x: x, y: y, p: V.KING, c: c }),
-            new PiPo({ x: x, y: rookPos, p: V.ROOK, c: c })
-          ],
-          end:
-            Math.abs(y - rookPos) <= 2
-              ? { x: x, y: rookPos }
-              : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
-        })
-      );
-    }
-
-    return moves;
-  }
-
   // TODO: appear/vanish description of a move is too verbose for Benedict.
   // => Would need a new "flipped" array, to be passed in Game.vue...
   getPotentialMovesFrom([x, y]) {
@@ -234,6 +122,11 @@ export const VariantRules = class BenedictRules extends ChessRules {
     return moves;
   }
 
+  // Since it's used just for the king, and there are no captures:
+  isAttacked(sq, color) {
+    return false;
+  }
+
   // No notion of check here:
   getCheckSquares() {
     return [];
diff --git a/client/src/variants/Berolina.js b/client/src/variants/Berolina.js
index 06592244..b14b19e8 100644
--- a/client/src/variants/Berolina.js
+++ b/client/src/variants/Berolina.js
@@ -1,6 +1,6 @@
 import { ChessRules } from "@/base_rules";
 
-export const VariantRules = class BerolinaRules extends ChessRules {
+export class BerolinaRules extends ChessRules {
   // En-passant after 2-sq jump
   getEpSquare(moveOrSquare) {
     if (!moveOrSquare) return undefined;
@@ -76,6 +76,7 @@ export const VariantRules = class BerolinaRules extends ChessRules {
           );
         }
         if (
+          V.PawnSpecs.twoSquares &&
           x == startRank &&
           y + 2 * shiftY >= 0 &&
           y + 2 * shiftY < sizeY &&
@@ -99,22 +100,25 @@ export const VariantRules = class BerolinaRules extends ChessRules {
         );
     }
 
-    // En passant
-    const Lep = this.epSquares.length;
-    const epSquare = this.epSquares[Lep - 1]; //always at least one element
-    if (
-      !!epSquare &&
-      epSquare[0].x == x + shiftX &&
-      epSquare[0].y == y
-    ) {
-      let enpassantMove = this.getBasicMove([x, y], [x + shiftX, y]);
-      enpassantMove.vanish.push({
-        x: x,
-        y: epSquare[1],
-        p: "p",
-        c: this.getColor(x, epSquare[1])
-      });
-      moves.push(enpassantMove);
+    // Next condition so that other variants could inherit from this class
+    if (V.PawnSpecs.enPassant) {
+      // En passant
+      const Lep = this.epSquares.length;
+      const epSquare = this.epSquares[Lep - 1]; //always at least one element
+      if (
+        !!epSquare &&
+        epSquare[0].x == x + shiftX &&
+        epSquare[0].y == y
+      ) {
+        let enpassantMove = this.getBasicMove([x, y], [x + shiftX, y]);
+        enpassantMove.vanish.push({
+          x: x,
+          y: epSquare[1],
+          p: "p",
+          c: this.getColor(x, epSquare[1])
+        });
+        moves.push(enpassantMove);
+      }
     }
 
     return moves;
@@ -133,6 +137,10 @@ export const VariantRules = class BerolinaRules extends ChessRules {
     return false;
   }
 
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
   getNotation(move) {
     const piece = this.getPiece(move.start.x, move.start.y);
     if (piece == V.PAWN) {
diff --git a/client/src/variants/Capture.js b/client/src/variants/Capture.js
index 5b286e0e..f0782a69 100644
--- a/client/src/variants/Capture.js
+++ b/client/src/variants/Capture.js
@@ -1,8 +1,6 @@
 import { ChessRules } from "@/base_rules";
-import { ArrayFun } from "@/utils/array";
-import { randInt } from "@/utils/alea";
 
-export const VariantRules = class LosersRules extends ChessRules {
+export class CaptureRules extends ChessRules {
   // Trim all non-capturing moves
   static KeepCaptures(moves) {
     return moves.filter(m => m.vanish.length == 2 && m.appear.length == 1);
@@ -17,8 +15,8 @@ export const VariantRules = class LosersRules extends ChessRules {
         if (
           this.board[i][j] != V.EMPTY &&
           this.getColor(i, j) != oppCol &&
-          this.getPotentialMovesFrom([i, j]).some(m =>
-            // Warning: duscard castle moves
+          this.filterValid(this.getPotentialMovesFrom([i, j])).some(m =>
+            // Warning: discard castle moves
             m.vanish.length == 2 && m.appear.length == 1)
         ) {
           return true;
diff --git a/client/src/variants/Checkered.js b/client/src/variants/Checkered.js
index 937b5741..d601d14a 100644
--- a/client/src/variants/Checkered.js
+++ b/client/src/variants/Checkered.js
@@ -1,6 +1,6 @@
 import { ChessRules, Move, PiPo } from "@/base_rules";
 
-export const VariantRules = class CheckeredRules extends ChessRules {
+export class CheckeredRules extends ChessRules {
   static board2fen(b) {
     const checkered_codes = {
       p: "s",
@@ -118,7 +118,7 @@ export const VariantRules = class CheckeredRules extends ChessRules {
   getPotentialMovesFrom([x, y]) {
     let standardMoves = super.getPotentialMovesFrom([x, y]);
     const lastRank = this.turn == "w" ? 0 : 7;
-    // King has to be treated differently (for castles)
+    // King is treated differently: it never turn checkered
     if (this.getPiece(x, y) == V.KING) return standardMoves;
     let moves = [];
     standardMoves.forEach(m => {
@@ -126,8 +126,9 @@ export const VariantRules = class CheckeredRules extends ChessRules {
         if (
           Math.abs(m.end.x - m.start.x) == 2 &&
           !this.pawnFlags[this.turn][m.start.y]
-        )
+        ) {
           return; //skip forbidden 2-squares jumps
+        }
         if (
           this.board[m.end.x][m.end.y] == V.EMPTY &&
           m.vanish.length == 2 &&
@@ -136,14 +137,16 @@ export const VariantRules = class CheckeredRules extends ChessRules {
           return; //checkered pawns cannot take en-passant
         }
       }
-      if (m.vanish.length == 1) moves.push(m);
-      // No capture
+      if (m.vanish.length == 1)
+        // No capture
+        moves.push(m);
       else {
         // A capture occured (m.vanish.length == 2)
         m.appear[0].c = "c";
         moves.push(m);
         if (
-          m.appear[0].p != m.vanish[1].p && //avoid promotions (already treated):
+          // Avoid promotions (already treated):
+          m.appear[0].p != m.vanish[1].p &&
           (m.vanish[0].p != V.PAWN || m.end.x != lastRank)
         ) {
           // Add transformation into captured piece
@@ -157,153 +160,13 @@ export const VariantRules = class CheckeredRules extends ChessRules {
   }
 
   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;
-    const pawnColor = this.getColor(x, y); //can be  checkered
-
-    const finalPieces =
-      x + shiftX == lastRank
-        ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]
-        : [V.PAWN];
-    if (this.board[x + shiftX][y] == V.EMPTY) {
-      // One square forward
-      for (let piece of finalPieces) {
-        moves.push(
-          this.getBasicMove([x, y], [x + shiftX, y], {
-            c: pawnColor,
-            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 + shiftY], {
-              c: pawnColor,
-              p: piece
-            })
-          );
-        }
-      }
-    }
-
-    // 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)
+    let moves = super.getPotentialPawnMoves([x, y]);
+    // Post-process: set right color for checkered moves
+    if (this.getColor(x, y) == 'c')
+      moves.forEach(m => {
+        m.appear[0].c = 'c'; //may be done twice if capture
+        m.vanish[0].c = 'c';
       });
-      moves.push(enpassantMove);
-    }
-
-    return moves;
-  }
-
-  // Same as in base_rules but with an array given to isAttacked:
-  getCastleMoves([x, y]) {
-    const c = this.getColor(x, y);
-    if (x != (c == "w" ? V.size.x - 1 : 0) || y != this.INIT_COL_KING[c])
-      return []; //x isn't first rank, or king has moved (shortcut)
-
-    // Castling ?
-    const oppCol = V.GetOppCol(c);
-    let moves = [];
-    let i = 0;
-    // King, then rook:
-    const finalSquares = [
-      [2, 3],
-      [V.size.y - 2, V.size.y - 3]
-    ];
-    castlingCheck: for (
-      let castleSide = 0;
-      castleSide < 2;
-      castleSide++ //large, then small
-    ) {
-      if (this.castleFlags[c][castleSide] >= V.size.y) continue;
-      // If this code is reached, rooks and king are on initial position
-
-      // Nothing on the path of the king ? (and no checks)
-      const finDist = finalSquares[castleSide][0] - y;
-      let step = finDist / Math.max(1, Math.abs(finDist));
-      i = y;
-      do {
-        if (
-          this.isAttacked([x, i], [oppCol]) ||
-          (this.board[x][i] != V.EMPTY &&
-            // NOTE: next check is enough, because of chessboard constraints
-            (this.getColor(x, i) != c ||
-              ![V.KING, V.ROOK].includes(this.getPiece(x, i))))
-        ) {
-          continue castlingCheck;
-        }
-        i += step;
-      } while (i != finalSquares[castleSide][0]);
-
-      // Nothing on the path to the rook?
-      step = castleSide == 0 ? -1 : 1;
-      const rookPos = this.castleFlags[c][castleSide];
-      for (i = y + step; i != rookPos; i += step) {
-        if (this.board[x][i] != V.EMPTY) continue castlingCheck;
-      }
-
-      // Nothing on final squares, except maybe king and castling rook?
-      for (i = 0; i < 2; i++) {
-        if (
-          this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
-          this.getPiece(x, finalSquares[castleSide][i]) != V.KING &&
-          finalSquares[castleSide][i] != rookPos
-        ) {
-          continue castlingCheck;
-        }
-      }
-
-      // If this code is reached, castle is valid
-      moves.push(
-        new Move({
-          appear: [
-            new PiPo({ x: x, y: finalSquares[castleSide][0], p: V.KING, c: c }),
-            new PiPo({ x: x, y: finalSquares[castleSide][1], p: V.ROOK, c: c })
-          ],
-          vanish: [
-            new PiPo({ x: x, y: y, p: V.KING, c: c }),
-            new PiPo({ x: x, y: rookPos, p: V.ROOK, c: c })
-          ],
-          end:
-            Math.abs(y - rookPos) <= 2
-              ? { x: x, y: rookPos }
-              : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
-        })
-      );
-    }
-
     return moves;
   }
 
@@ -375,6 +238,7 @@ export const VariantRules = class CheckeredRules extends ChessRules {
 
   // colors: array, generally 'w' and 'c' or 'b' and 'c'
   isAttacked(sq, colors) {
+    if (!Array.isArray(colors)) colors = [colors];
     return (
       this.isAttackedByPawn(sq, colors) ||
       this.isAttackedByRook(sq, colors) ||
diff --git a/client/src/variants/Chess960.js b/client/src/variants/Chess960.js
index 7e10e2a5..3f6af502 100644
--- a/client/src/variants/Chess960.js
+++ b/client/src/variants/Chess960.js
@@ -1,4 +1,4 @@
 import { ChessRules } from "@/base_rules";
-export const VariantRules = class Chess960Rules extends ChessRules {
+export class Chess960Rules extends ChessRules {
   // Standard rules
 };
diff --git a/client/src/variants/Circular.js b/client/src/variants/Circular.js
index 61997f29..4338c91f 100644
--- a/client/src/variants/Circular.js
+++ b/client/src/variants/Circular.js
@@ -1,8 +1,8 @@
 import { ChessRules, PiPo, Move } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
-import { randInt, shuffle } from "@/utils/alea";
+import { shuffle } from "@/utils/alea";
 
-export const VariantRules = class CircularRules extends ChessRules {
+export class CircularRules extends ChessRules {
   static get HasCastle() {
     return false;
   }
@@ -39,53 +39,25 @@ export const VariantRules = class CircularRules extends ChessRules {
       return "8/8/pppppppp/rnbqkbnr/8/8/PPPPPPPP/RNBQKBNR w 0 1111111111111111";
 
     let pieces = { w: new Array(8), b: new Array(8) };
-    // Shuffle pieces on first and fifth rank
+    // Shuffle pieces on first and last rank
     for (let c of ["w", "b"]) {
       if (c == 'b' && randomness == 1) {
         pieces['b'] = pieces['w'];
         break;
       }
 
-      let positions = ArrayFun.range(8);
-
-      // Get random squares for bishops
-      let randIndex = 2 * randInt(4);
-      const bishop1Pos = positions[randIndex];
-      // The second bishop must be on a square of different color
-      let randIndex_tmp = 2 * randInt(4) + 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(6);
-      const knight1Pos = positions[randIndex];
-      positions.splice(randIndex, 1);
-      randIndex = randInt(5);
-      const knight2Pos = positions[randIndex];
-      positions.splice(randIndex, 1);
-
-      // Get random square for queen
-      randIndex = randInt(4);
-      const queenPos = positions[randIndex];
-      positions.splice(randIndex, 1);
-
-      // Rooks and king positions are now fixed,
-      // because of the ordering rook-king-rook
-      const rook1Pos = positions[0];
-      const kingPos = positions[1];
-      const rook2Pos = positions[2];
-
-      // Finally put the shuffled pieces in the board array
-      pieces[c][rook1Pos] = "r";
-      pieces[c][knight1Pos] = "n";
-      pieces[c][bishop1Pos] = "b";
-      pieces[c][queenPos] = "q";
-      pieces[c][kingPos] = "k";
-      pieces[c][bishop2Pos] = "b";
-      pieces[c][knight2Pos] = "n";
-      pieces[c][rook2Pos] = "r";
+      // Get random squares for every piece, totally freely
+      let positions = shuffle(ArrayFun.range(8));
+      const composition = ['b', 'b', 'r', 'r', 'n', 'n', 'k', 'q'];
+      const rem2 = positions[0] % 2;
+      if (rem2 == positions[1] % 2) {
+        // Fix bishops (on different colors)
+        for (let i=2; i<8; i++) {
+          if (positions[i] % 2 != rem2)
+            [positions[1], positions[i]] = [positions[i], positions[1]];
+        }
+      }
+      for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
     }
     return (
       "8/8/pppppppp/" +
diff --git a/client/src/variants/Crazyhouse.js b/client/src/variants/Crazyhouse.js
index fb8c7441..321bf4cf 100644
--- a/client/src/variants/Crazyhouse.js
+++ b/client/src/variants/Crazyhouse.js
@@ -1,7 +1,7 @@
 import { ChessRules, PiPo, Move } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
 
-export const VariantRules = class CrazyhouseRules extends ChessRules {
+export class CrazyhouseRules extends ChessRules {
   static IsGoodFen(fen) {
     if (!ChessRules.IsGoodFen(fen)) return false;
     const fenParsed = V.ParseFen(fen);
diff --git a/client/src/variants/Cylinder.js b/client/src/variants/Cylinder.js
index 44d1ab16..f26f3e62 100644
--- a/client/src/variants/Cylinder.js
+++ b/client/src/variants/Cylinder.js
@@ -2,7 +2,7 @@ import { ChessRules, PiPo, Move } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
 import { randInt, shuffle } from "@/utils/alea";
 
-export const VariantRules = class CylinderRules extends ChessRules {
+export class CylinderRules extends ChessRules {
   // Output basically x % 8 (circular board)
   static ComputeY(y) {
     let res = y % V.size.y;
diff --git a/client/src/variants/Dark.js b/client/src/variants/Dark.js
index a7edb916..3bca2340 100644
--- a/client/src/variants/Dark.js
+++ b/client/src/variants/Dark.js
@@ -2,7 +2,7 @@ import { ChessRules } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
 import { randInt } from "@/utils/alea";
 
-export const VariantRules = class DarkRules extends ChessRules {
+export class DarkRules extends ChessRules {
   // Analyse in Dark mode makes no sense
   static get CanAnalyze() {
     return false;
diff --git a/client/src/variants/Eightpieces.js b/client/src/variants/Eightpieces.js
index a0eed50d..fe36ab24 100644
--- a/client/src/variants/Eightpieces.js
+++ b/client/src/variants/Eightpieces.js
@@ -2,7 +2,7 @@ import { ArrayFun } from "@/utils/array";
 import { randInt } from "@/utils/alea";
 import { ChessRules, PiPo, Move } from "@/base_rules";
 
-export const VariantRules = class EightpiecesRules extends ChessRules {
+export class EightpiecesRules extends ChessRules {
   static get JAILER() {
     return "j";
   }
diff --git a/client/src/variants/Enpassant.js b/client/src/variants/Enpassant.js
index dc181cab..ee04cf4f 100644
--- a/client/src/variants/Enpassant.js
+++ b/client/src/variants/Enpassant.js
@@ -1,7 +1,6 @@
 import { ChessRules, PiPo, Move } from "@/base_rules";
 
-export const VariantRules = class EnpassantRules extends ChessRules {
-
+export class EnpassantRules extends ChessRules {
   static IsGoodEnpassant(enpassant) {
     if (enpassant != "-") {
       const squares = enpassant.split(",");
@@ -113,60 +112,10 @@ export const VariantRules = class EnpassantRules extends ChessRules {
     return moves;
   }
 
-  // TODO: this getPotentialPawnMovesFrom() is mostly duplicated:
-  // it could be split in "capture", "promotion", "enpassant"...
-  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;
-
-    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 + shiftY], {
-              c: color,
-              p: piece
-            })
-          );
-        }
-      }
-    }
-
-    // En passant
+  getEnpassantCaptures([x, y], shiftX) {
     const Lep = this.epSquares.length;
     const squares = this.epSquares[Lep - 1];
+    let moves = [];
     if (!!squares) {
       const S = squares.length;
       const taken = squares[S-1];
@@ -185,7 +134,6 @@ export const VariantRules = class EnpassantRules extends ChessRules {
         }
       });
     }
-
     return moves;
   }
 
diff --git a/client/src/variants/Extinction.js b/client/src/variants/Extinction.js
index 3fa53276..e2571378 100644
--- a/client/src/variants/Extinction.js
+++ b/client/src/variants/Extinction.js
@@ -1,6 +1,14 @@
 import { ChessRules } from "@/base_rules";
 
-export const VariantRules = class ExtinctionRules extends ChessRules {
+export class ExtinctionRules extends ChessRules {
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      { promotions: ChessRules.PawnSpecs.promotions.concat([V.KING]) }
+    );
+  }
+
   static IsGoodPosition(position) {
     if (!ChessRules.IsGoodPosition(position))
       return false;
@@ -43,43 +51,6 @@ export const VariantRules = class ExtinctionRules extends ChessRules {
     };
   }
 
-  getPotentialPawnMoves([x, y]) {
-    let moves = super.getPotentialPawnMoves([x, y]);
-    // Add potential promotions into king
-    const color = this.turn;
-    const shift = color == "w" ? -1 : 1;
-    const lastRank = color == "w" ? 0 : V.size.x - 1;
-
-    if (x + shift == lastRank) {
-      // Normal move
-      if (this.board[x + shift][y] == V.EMPTY)
-        moves.push(
-          this.getBasicMove([x, y], [x + shift, y], { c: color, p: V.KING })
-        );
-      // Captures
-      if (
-        y > 0 &&
-        this.board[x + shift][y - 1] != V.EMPTY &&
-        this.canTake([x, y], [x + shift, y - 1])
-      ) {
-        moves.push(
-          this.getBasicMove([x, y], [x + shift, y - 1], { c: color, p: V.KING })
-        );
-      }
-      if (
-        y < V.size.y - 1 &&
-        this.board[x + shift][y + 1] != V.EMPTY &&
-        this.canTake([x, y], [x + shift, y + 1])
-      ) {
-        moves.push(
-          this.getBasicMove([x, y], [x + shift, y + 1], { c: color, p: V.KING })
-        );
-      }
-    }
-
-    return moves;
-  }
-
   // TODO: verify this assertion
   atLeastOneMove() {
     return true; //always at least one possible move
diff --git a/client/src/variants/Grand.js b/client/src/variants/Grand.js
index d4f620f7..5523244b 100644
--- a/client/src/variants/Grand.js
+++ b/client/src/variants/Grand.js
@@ -4,7 +4,7 @@ import { randInt } from "@/utils/alea";
 
 // NOTE: initial setup differs from the original; see
 // https://www.chessvariants.com/large.dir/freeling.html
-export const VariantRules = class GrandRules extends ChessRules {
+export class GrandRules extends ChessRules {
   static IsGoodFen(fen) {
     if (!ChessRules.IsGoodFen(fen)) return false;
     const fenParsed = V.ParseFen(fen);
@@ -223,7 +223,7 @@ export const VariantRules = class GrandRules extends ChessRules {
       for (let epsq of epSquare) {
         // TODO: some redundant checks
         if (epsq.x == x + shiftX && Math.abs(epsq.y - y) == 1) {
-          var enpassantMove = this.getBasicMove([x, y], [epsq.x, epsq.y]);
+          let enpassantMove = this.getBasicMove([x, y], [epsq.x, epsq.y]);
           // WARNING: the captured pawn may be diagonally behind us,
           // if it's a 3-squares jump and we take on 1st passing square
           const px = this.board[x][epsq.y] != V.EMPTY ? x : x - shiftX;
diff --git a/client/src/variants/Grasshopper.js b/client/src/variants/Grasshopper.js
index ec52d9b3..fb8c27cd 100644
--- a/client/src/variants/Grasshopper.js
+++ b/client/src/variants/Grasshopper.js
@@ -2,11 +2,22 @@ import { ChessRules } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
 import { randInt } from "@/utils/alea";
 
-export const VariantRules = class GrasshopperRules extends ChessRules {
+export class GrasshopperRules extends ChessRules {
   static get HasEnpassant() {
     return false;
   }
 
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      {
+        twoSquares: false,
+        promotions: ChessRules.PawnSpecs.promotions.concat([V.GRASSHOPPER])
+      }
+    );
+  }
+
   static get GRASSHOPPER() {
     return "g";
   }
diff --git a/client/src/variants/Hidden.js b/client/src/variants/Hidden.js
index 8a5858ea..c060310c 100644
--- a/client/src/variants/Hidden.js
+++ b/client/src/variants/Hidden.js
@@ -2,7 +2,7 @@ import { ChessRules, PiPo, Move } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
 import { randInt } from "@/utils/alea";
 
-export const VariantRules = class HiddenRules extends ChessRules {
+export class HiddenRules extends ChessRules {
   static get HasFlags() {
     return false;
   }
diff --git a/client/src/variants/Hiddenqueen.js b/client/src/variants/Hiddenqueen.js
index 15d9b1e9..8db5a50e 100644
--- a/client/src/variants/Hiddenqueen.js
+++ b/client/src/variants/Hiddenqueen.js
@@ -2,7 +2,7 @@ import { ChessRules, PiPo, Move } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
 import { randInt } from "@/utils/alea";
 
-export const VariantRules = class HiddenqueenRules extends ChessRules {
+export class HiddenqueenRules extends ChessRules {
   // Analyse in Hiddenqueen mode makes no sense
   static get CanAnalyze() {
     return false;
@@ -87,79 +87,13 @@ export const VariantRules = class HiddenqueenRules extends ChessRules {
     return super.getPotentialMovesFrom([x, y]);
   }
 
-  // TODO: find a more general way to describe pawn movements to avoid
-  // re-writing almost the same function for several variants.
   getPotentialPawnMoves([x, y]) {
-    const color = this.turn;
     const piece = this.getPiece(x, y);
-    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;
-
-    const finalPieces =
-      x + shiftX == lastRank
-        ? piece == V.PAWN
-          ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]
-          : [V.QUEEN] //hidden queen revealed
-        : [piece]; //V.PAWN
-    if (this.board[x + shiftX][y] == V.EMPTY) {
-      // One square forward
-      for (let p of finalPieces) {
-        moves.push(
-          this.getBasicMove([x, y], [x + shiftX, y], {
-            c: color,
-            p: p
-          })
-        );
-      }
-      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 p of finalPieces) {
-          moves.push(
-            this.getBasicMove([x, y], [x + shiftX, y + shiftY], {
-              c: color,
-              p: p
-            })
-          );
-        }
-      }
-    }
-
-    // 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;
+    const promotions =
+      piece == V.PAWN
+        ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]
+        : [V.QUEEN]; //hidden queen revealed
+    return super.getPotentialPawnMoves([x, y], promotions);
   }
 
   getPossibleMovesFrom(sq) {
diff --git a/client/src/variants/Knightmate.js b/client/src/variants/Knightmate.js
index a8d6dc6c..593fcc09 100644
--- a/client/src/variants/Knightmate.js
+++ b/client/src/variants/Knightmate.js
@@ -2,7 +2,7 @@ import { ChessRules } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
 import { randInt } from "@/utils/alea";
 
-export const VariantRules = class KnightmateRules extends ChessRules {
+export class KnightmateRules extends ChessRules {
   static get COMMONER() {
     return "c";
   }
diff --git a/client/src/variants/Knightrelay1.js b/client/src/variants/Knightrelay1.js
index 3e7bf38c..1d1ab527 100644
--- a/client/src/variants/Knightrelay1.js
+++ b/client/src/variants/Knightrelay1.js
@@ -1,6 +1,6 @@
 import { ChessRules } from "@/base_rules";
 
-export const VariantRules = class Knightrelay1Rules extends ChessRules {
+export class Knightrelay1Rules extends ChessRules {
   static get HasEnpassant() {
     return false;
   }
diff --git a/client/src/variants/Knightrelay2.js b/client/src/variants/Knightrelay2.js
index 9fa5dcca..0fc6c331 100644
--- a/client/src/variants/Knightrelay2.js
+++ b/client/src/variants/Knightrelay2.js
@@ -1,6 +1,6 @@
 import { ChessRules } from "@/base_rules";
 
-export const VariantRules = class Knightrelay2Rules extends ChessRules {
+export class Knightrelay2Rules extends ChessRules {
   getPotentialMovesFrom([x, y]) {
     let moves = super.getPotentialMovesFrom([x, y]);
 
diff --git a/client/src/variants/Losers.js b/client/src/variants/Losers.js
index d752f984..1c087eff 100644
--- a/client/src/variants/Losers.js
+++ b/client/src/variants/Losers.js
@@ -2,7 +2,7 @@ import { ChessRules } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
 import { randInt } from "@/utils/alea";
 
-export const VariantRules = class LosersRules extends ChessRules {
+export class LosersRules extends ChessRules {
   // Trim all non-capturing moves
   static KeepCaptures(moves) {
     return moves.filter(m => m.vanish.length == 2);
diff --git a/client/src/variants/Magnetic.js b/client/src/variants/Magnetic.js
index a59ce411..a6150cb5 100644
--- a/client/src/variants/Magnetic.js
+++ b/client/src/variants/Magnetic.js
@@ -1,6 +1,6 @@
 import { ChessRules, PiPo } from "@/base_rules";
 
-export const VariantRules = class MagneticRules extends ChessRules {
+export class MagneticRules extends ChessRules {
   static get HasEnpassant() {
     return false;
   }
diff --git a/client/src/variants/Marseille.js b/client/src/variants/Marseille.js
index c3bec32f..ed9965b0 100644
--- a/client/src/variants/Marseille.js
+++ b/client/src/variants/Marseille.js
@@ -1,7 +1,7 @@
 import { ChessRules } from "@/base_rules";
 import { randInt } from "@/utils/alea";
 
-export const VariantRules = class MarseilleRules extends ChessRules {
+export class MarseilleRules extends ChessRules {
   static IsGoodEnpassant(enpassant) {
     const squares = enpassant.split(",");
     if (squares.length > 2) return false;
@@ -43,52 +43,8 @@ export const VariantRules = class MarseilleRules extends ChessRules {
     this.subTurn = fullTurn[1] || 1;
   }
 
-  getPotentialPawnMoves([x, y]) {
-    const color = this.turn;
+  getEnpassantCaptures([x, y], shiftX) {
     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 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 })
-        );
-      }
-      // 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: color,
-              p: piece
-            })
-          );
-        }
-      }
-    }
-
     // En passant: always OK if subturn 1,
     // OK on subturn 2 only if enPassant was played at subturn 1
     // (and if there are two e.p. squares available).
@@ -99,7 +55,7 @@ export const VariantRules = class MarseilleRules extends ChessRules {
       if (sq) epSqs.push(sq);
     });
     if (epSqs.length == 0) return moves;
-    const oppCol = V.GetOppCol(color);
+    const oppCol = V.GetOppCol(this.getColor(x, y));
     for (let sq of epSqs) {
       if (
         this.subTurn == 1 ||
@@ -126,7 +82,6 @@ export const VariantRules = class MarseilleRules extends ChessRules {
         }
       }
     }
-
     return moves;
   }
 
diff --git a/client/src/variants/Racingkings.js b/client/src/variants/Racingkings.js
index f1161fce..11457485 100644
--- a/client/src/variants/Racingkings.js
+++ b/client/src/variants/Racingkings.js
@@ -1,6 +1,6 @@
 import { ChessRules } from "@/base_rules";
 
-export const VariantRules = class RacingkingsRules extends ChessRules {
+export class RacingkingsRules extends ChessRules {
   static get HasFlags() {
     return false;
   }
diff --git a/client/src/variants/Recycle.js b/client/src/variants/Recycle.js
index d91e8248..8980c721 100644
--- a/client/src/variants/Recycle.js
+++ b/client/src/variants/Recycle.js
@@ -1,7 +1,15 @@
 import { ChessRules, PiPo, Move } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
 
-export const VariantRules = class RecycleRules extends ChessRules {
+export class RecycleRules extends ChessRules {
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      { promotions: [V.PAWN] } //in fact, none
+    );
+  }
+
   static IsGoodFen(fen) {
     if (!ChessRules.IsGoodFen(fen)) return false;
     const fenParsed = V.ParseFen(fen);
@@ -131,66 +139,13 @@ export const VariantRules = class RecycleRules extends ChessRules {
   }
 
   getPotentialPawnMoves([x, y]) {
+    let moves = super.getPotentialPawnMoves([x, y]);
+    // Remove pawns on 8th rank ("fallen"):
     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();
-      });
-    }
-
+    const lastRank = (color == "w" ? 0 : V.size.x - 1);
+    moves.forEach(m => {
+      if (m.appear[0].x == lastRank) m.appear.pop();
+    });
     return moves;
   }
 
@@ -223,10 +178,10 @@ export const VariantRules = class RecycleRules extends ChessRules {
     return this.getPiece(x2, y2) != V.KING;
   }
 
-  postPlay(move) {
-    super.postPlay(move);
+  prePlay(move) {
+    super.prePlay(move);
     if (move.vanish.length == 2 && move.appear.length == 2) return; //skip castle
-    const color = move.appear[0].c;
+    const color = this.turn;
     if (move.vanish.length == 0) this.reserve[color][move.appear[0].p]--;
     else if (move.vanish.length == 2 && move.vanish[1].c == color)
       // Self-capture
diff --git a/client/src/variants/Rifle.js b/client/src/variants/Rifle.js
index 1cdbbbe4..98661ae7 100644
--- a/client/src/variants/Rifle.js
+++ b/client/src/variants/Rifle.js
@@ -1,6 +1,6 @@
 import { ChessRules, PiPo, Move } from "@/base_rules";
 
-export const VariantRules = class RifleRules extends ChessRules {
+export class RifleRules extends ChessRules {
   getEpSquare(moveOrSquare) {
     if (typeof moveOrSquare !== "object" || moveOrSquare.appear.length > 0)
       return super.getEpSquare(moveOrSquare);
@@ -49,54 +49,8 @@ export const VariantRules = class RifleRules extends ChessRules {
     return mv;
   }
 
-  getPotentialPawnMoves([x, y]) {
-    const color = this.turn;
+  getEnpassantCaptures([x, y], shiftX) {
     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;
-
-    const finalPieces =
-      x + shiftX == lastRank
-        ? [V.ROOK, V.KNIGHT, V.BISHOP, V.QUEEN]
-        : [V.PAWN];
-    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
-      ) {
-        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: color,
-              p: piece
-            })
-          );
-        }
-      }
-    }
-
-    // En passant
     const Lep = this.epSquares.length;
     const epSquare = this.epSquares[Lep - 1]; //always at least one element
     if (
@@ -118,7 +72,10 @@ export const VariantRules = class RifleRules extends ChessRules {
       });
       moves.push(enpassantMove);
     }
-
     return moves;
   }
+
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
 };
diff --git a/client/src/variants/Royalrace.js b/client/src/variants/Royalrace.js
index c9095558..9b6575ac 100644
--- a/client/src/variants/Royalrace.js
+++ b/client/src/variants/Royalrace.js
@@ -2,7 +2,7 @@ import { ChessRules } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
 import { randInt, shuffle } from "@/utils/alea";
 
-export const VariantRules = class RoyalraceRules extends ChessRules {
+export class RoyalraceRules extends ChessRules {
   static get HasFlags() {
     return false;
   }
diff --git a/client/src/variants/Shatranj.js b/client/src/variants/Shatranj.js
index 45a34c04..00b14564 100644
--- a/client/src/variants/Shatranj.js
+++ b/client/src/variants/Shatranj.js
@@ -1,6 +1,6 @@
 import { ChessRules } from "@/base_rules";
 
-export const VariantRules = class ShatranjRules extends ChessRules {
+export class ShatranjRules extends ChessRules {
   static get HasFlags() {
     return false;
   }
@@ -13,6 +13,17 @@ export const VariantRules = class ShatranjRules extends ChessRules {
     return false;
   }
 
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      {
+        twoSquares: false,
+        promotions: [V.QUEEN]
+      }
+    );
+  }
+
   static get ElephantSteps() {
     return [
       [-2, -2],
@@ -27,45 +38,6 @@ export const VariantRules = class ShatranjRules extends ChessRules {
     return ChessRules.GenRandInitFen(randomness).slice(0, -7);
   }
 
-  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
diff --git a/client/src/variants/Suction.js b/client/src/variants/Suction.js
index a39bec0b..d15a7c84 100644
--- a/client/src/variants/Suction.js
+++ b/client/src/variants/Suction.js
@@ -1,6 +1,6 @@
 import { ChessRules, PiPo, Move } from "@/base_rules";
 
-export const VariantRules = class SuctionRules extends ChessRules {
+export class SuctionRules extends ChessRules {
   static get HasFlags() {
     return false;
   }
@@ -85,50 +85,8 @@ export const VariantRules = class SuctionRules extends ChessRules {
     return mv;
   }
 
-  getPotentialPawnMoves([x, y]) {
-    const color = this.turn;
+  getEnpassantCaptures([x, y], shiftX) {
     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 firstRank = color == "w" ? sizeX - 1 : 0;
-
-    if (x + shiftX >= 0 && x + shiftX < sizeX) {
-      // One square forward
-      if (this.board[x + shiftX][y] == V.EMPTY) {
-        moves.push(
-          this.getBasicMove([x, y], [x + shiftX, y], {
-            c: color,
-            p: "p"
-          })
-        );
-        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]));
-        }
-      }
-      // Swaps
-      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: "p"
-            })
-          );
-        }
-      }
-    }
-
-    // En passant
     const Lep = this.epSquares.length;
     const epSquare = this.epSquares[Lep - 1]; //always at least one element
     if (
@@ -152,7 +110,6 @@ export const VariantRules = class SuctionRules extends ChessRules {
       });
       moves.push(enpassantMove);
     }
-
     return moves;
   }
 
diff --git a/client/src/variants/Suicide.js b/client/src/variants/Suicide.js
index 4bb5d36b..1304ba98 100644
--- a/client/src/variants/Suicide.js
+++ b/client/src/variants/Suicide.js
@@ -1,8 +1,8 @@
 import { ChessRules } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
-import { randInt } from "@/utils/alea";
+import { shuffle } from "@/utils/alea";
 
-export const VariantRules = class SuicideRules extends ChessRules {
+export class SuicideRules extends ChessRules {
   static get HasFlags() {
     return false;
   }
@@ -11,41 +11,12 @@ export const VariantRules = class SuicideRules extends ChessRules {
     return false;
   }
 
-  getPotentialPawnMoves([x, y]) {
-    let moves = super.getPotentialPawnMoves([x, y]);
-
-    // Complete with promotion(s) into king, if possible
-    const color = this.turn;
-    const shift = color == "w" ? -1 : 1;
-    const lastRank = color == "w" ? 0 : V.size.x - 1;
-    if (x + shift == lastRank) {
-      // Normal move
-      if (this.board[x + shift][y] == V.EMPTY)
-        moves.push(
-          this.getBasicMove([x, y], [x + shift, y], { c: color, p: V.KING })
-        );
-      // Captures
-      if (
-        y > 0 &&
-        this.canTake([x, y], [x + shift, y - 1]) &&
-        this.board[x + shift][y - 1] != V.EMPTY
-      ) {
-        moves.push(
-          this.getBasicMove([x, y], [x + shift, y - 1], { c: color, p: V.KING })
-        );
-      }
-      if (
-        y < V.size.y - 1 &&
-        this.canTake([x, y], [x + shift, y + 1]) &&
-        this.board[x + shift][y + 1] != V.EMPTY
-      ) {
-        moves.push(
-          this.getBasicMove([x, y], [x + shift, y + 1], { c: color, p: V.KING })
-        );
-      }
-    }
-
-    return moves;
+  static get PawnSpecs() {
+    return Object.assign(
+      {},
+      ChessRules.PawnSpecs,
+      { promotions: ChessRules.PawnSpecs.promotions.concat([V.KING]) }
+    );
   }
 
   // Trim all non-capturing moves (not the most efficient, but easy)
@@ -152,49 +123,18 @@ export const VariantRules = class SuicideRules extends ChessRules {
         break;
       }
 
-      let positions = ArrayFun.range(8);
-
-      // Get random squares for bishops
-      let randIndex = 2 * randInt(4);
-      let bishop1Pos = positions[randIndex];
-      // The second bishop must be on a square of different color
-      let randIndex_tmp = 2 * randInt(4) + 1;
-      let 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(6);
-      let knight1Pos = positions[randIndex];
-      positions.splice(randIndex, 1);
-      randIndex = randInt(5);
-      let knight2Pos = positions[randIndex];
-      positions.splice(randIndex, 1);
-
-      // Get random square for queen
-      randIndex = randInt(4);
-      let queenPos = positions[randIndex];
-      positions.splice(randIndex, 1);
-
-      // Random square for king (no castle)
-      randIndex = randInt(3);
-      let kingPos = positions[randIndex];
-      positions.splice(randIndex, 1);
-
-      // Rooks positions are now fixed
-      let rook1Pos = positions[0];
-      let rook2Pos = positions[1];
-
-      // Finally put the shuffled pieces in the board array
-      pieces[c][rook1Pos] = "r";
-      pieces[c][knight1Pos] = "n";
-      pieces[c][bishop1Pos] = "b";
-      pieces[c][queenPos] = "q";
-      pieces[c][kingPos] = "k";
-      pieces[c][bishop2Pos] = "b";
-      pieces[c][knight2Pos] = "n";
-      pieces[c][rook2Pos] = "r";
+      // Get random squares for every piece, totally freely
+      let positions = shuffle(ArrayFun.range(8));
+      const composition = ['b', 'b', 'r', 'r', 'n', 'n', 'k', 'q'];
+      const rem2 = positions[0] % 2;
+      if (rem2 == positions[1] % 2) {
+        // Fix bishops (on different colors)
+        for (let i=2; i<8; i++) {
+          if (positions[i] % 2 != rem2)
+            [positions[1], positions[i]] = [positions[i], positions[1]];
+        }
+      }
+      for (let i = 0; i < 8; i++) pieces[c][positions[i]] = composition[i];
     }
     return (
       pieces["b"].join("") +
diff --git a/client/src/variants/Threechecks.js b/client/src/variants/Threechecks.js
index 3aa6db98..f9fd4d46 100644
--- a/client/src/variants/Threechecks.js
+++ b/client/src/variants/Threechecks.js
@@ -1,6 +1,6 @@
 import { ChessRules } from "@/base_rules";
 
-export const VariantRules = class ThreechecksRules extends ChessRules {
+export class ThreechecksRules extends ChessRules {
   static IsGoodFlags(flags) {
     // 4 for castle + 2 for checks (0,1 or 2)
     return !!flags.match(/^[01]{4,4}[012]{2,2}$/);
diff --git a/client/src/variants/Upsidedown.js b/client/src/variants/Upsidedown.js
index e8242004..20768240 100644
--- a/client/src/variants/Upsidedown.js
+++ b/client/src/variants/Upsidedown.js
@@ -2,7 +2,7 @@ import { ChessRules } from "@/base_rules";
 import { randInt } from "@/utils/alea";
 import { ArrayFun } from "@/utils/array";
 
-export const VariantRules = class UpsidedownRules extends ChessRules {
+export class UpsidedownRules extends ChessRules {
   static get HasFlags() {
     return false;
   }
diff --git a/client/src/variants/Wildebeest.js b/client/src/variants/Wildebeest.js
index f4e3b83c..8192fa04 100644
--- a/client/src/variants/Wildebeest.js
+++ b/client/src/variants/Wildebeest.js
@@ -2,7 +2,7 @@ import { ChessRules } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
 import { sample, randInt } from "@/utils/alea";
 
-export const VariantRules = class WildebeestRules extends ChessRules {
+export class WildebeestRules extends ChessRules {
   static get size() {
     return { x: 10, y: 11 };
   }
diff --git a/client/src/variants/Wormhole.js b/client/src/variants/Wormhole.js
index 41c934a3..f47fea39 100644
--- a/client/src/variants/Wormhole.js
+++ b/client/src/variants/Wormhole.js
@@ -1,6 +1,6 @@
 import { ChessRules } from "@/base_rules";
 
-export const VariantRules = class WormholeRules extends ChessRules {
+export class WormholeRules extends ChessRules {
   static get HasFlags() {
     return false;
   }
diff --git a/client/src/variants/Zen.js b/client/src/variants/Zen.js
index bc51e6ad..56e7108b 100644
--- a/client/src/variants/Zen.js
+++ b/client/src/variants/Zen.js
@@ -1,6 +1,6 @@
 import { ChessRules } from "@/base_rules";
 
-export const VariantRules = class ZenRules extends ChessRules {
+export class ZenRules extends ChessRules {
   // NOTE: enPassant, if enabled, would need to redefine carefully getEpSquare
   static get HasEnpassant() {
     return false;
diff --git a/client/src/views/Analyse.vue b/client/src/views/Analyse.vue
index becc4de0..484b8046 100644
--- a/client/src/views/Analyse.vue
+++ b/client/src/views/Analyse.vue
@@ -71,7 +71,7 @@ export default {
       // Obtain VariantRules object
       await import("@/variants/" + this.gameRef.vname + ".js")
       .then((vModule) => {
-        window.V = vModule.VariantRules;
+        window.V = vModule[this.gameRef.vname + "Rules"];
         if (!V.CanAnalyze)
           // Late check, in case the user tried to enter URL by hand
           this.alertAndQuit("Analysis disabled for this variant");
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index bceb361c..0b31bc3b 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -913,156 +913,161 @@ export default {
     //  - from server (one correspondance game I play[ed] or not)
     //  - from remote peer (one live game I don't play, finished or not)
     loadGame: function(game, callback) {
-      const afterRetrieval = async (game) => {
-        const vModule = await import("@/variants/" + game.vname + ".js");
-        window.V = vModule.VariantRules;
-        this.vr = new V(game.fen);
-        const gtype = this.getGameType(game);
-        const tc = extractTime(game.cadence);
-        const myIdx = game.players.findIndex(p => {
-          return p.sid == this.st.user.sid || p.id == this.st.user.id;
-        });
-        const mycolor = [undefined, "w", "b"][myIdx + 1]; //undefined for observers
-        if (!game.chats) game.chats = []; //live games don't have chat history
-        if (gtype == "corr") {
-          // NOTE: clocks in seconds, initime in milliseconds
-          game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of
-          game.clocks = [tc.mainTime, tc.mainTime];
-          const L = game.moves.length;
-          if (game.score == "*") {
-            // Set clocks + initime
-            game.initime = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
-            if (L >= 1) game.initime[L % 2] = game.moves[L-1].played;
-            // NOTE: game.clocks shouldn't be computed right now:
-            // job will be done in re_setClocks() called soon below.
-          }
-          // Sort chat messages from newest to oldest
-          game.chats.sort((c1, c2) => {
-            return c2.added - c1.added;
-          });
-          if (myIdx >= 0 && game.score == "*" && game.chats.length > 0) {
-            // Did a chat message arrive after my last move?
-            let dtLastMove = 0;
-            if (L == 1 && myIdx == 0)
-              dtLastMove = game.moves[0].played;
-            else if (L >= 2) {
-              if (L % 2 == 0) {
-                // It's now white turn
-                dtLastMove = game.moves[L-1-(1-myIdx)].played;
-              } else {
-                // Black turn:
-                dtLastMove = game.moves[L-1-myIdx].played;
-              }
-            }
-            if (dtLastMove < game.chats[0].added)
-              document.getElementById("chatBtn").classList.add("somethingnew");
-          }
-          // Now that we used idx and played, re-format moves as for live games
-          game.moves = game.moves.map(m => m.squares);
-        }
-        if (gtype == "live" && game.clocks[0] < 0) {
-          // Game is unstarted. clocks and initime are ignored until move 2
-          game.clocks = [tc.mainTime, tc.mainTime];
+      this.vr = new V(game.fen);
+      const gtype = this.getGameType(game);
+      const tc = extractTime(game.cadence);
+      const myIdx = game.players.findIndex(p => {
+        return p.sid == this.st.user.sid || p.id == this.st.user.id;
+      });
+      const mycolor = [undefined, "w", "b"][myIdx + 1]; //undefined for observers
+      if (!game.chats) game.chats = []; //live games don't have chat history
+      if (gtype == "corr") {
+        // NOTE: clocks in seconds, initime in milliseconds
+        game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of
+        game.clocks = [tc.mainTime, tc.mainTime];
+        const L = game.moves.length;
+        if (game.score == "*") {
+          // Set clocks + initime
           game.initime = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
-          if (myIdx >= 0) {
-            // I play in this live game
-            GameStorage.update(game.id, {
-              clocks: game.clocks,
-              initime: game.initime
-            });
-          }
+          if (L >= 1) game.initime[L % 2] = game.moves[L-1].played;
+          // NOTE: game.clocks shouldn't be computed right now:
+          // job will be done in re_setClocks() called soon below.
         }
-        // TODO: merge next 2 "if" conditions
-        if (!!game.drawOffer) {
-          if (game.drawOffer == "t")
-            // Three repetitions
-            this.drawOffer = "threerep";
-          else {
-            // Draw offered by any of the players:
-            if (myIdx < 0) this.drawOffer = "received";
-            else {
-              // I play in this game:
-              if (
-                (game.drawOffer == "w" && myIdx == 0) ||
-                (game.drawOffer == "b" && myIdx == 1)
-              )
-                this.drawOffer = "sent";
-              else this.drawOffer = "received";
+        // Sort chat messages from newest to oldest
+        game.chats.sort((c1, c2) => {
+          return c2.added - c1.added;
+        });
+        if (myIdx >= 0 && game.score == "*" && game.chats.length > 0) {
+          // Did a chat message arrive after my last move?
+          let dtLastMove = 0;
+          if (L == 1 && myIdx == 0)
+            dtLastMove = game.moves[0].played;
+          else if (L >= 2) {
+            if (L % 2 == 0) {
+              // It's now white turn
+              dtLastMove = game.moves[L-1-(1-myIdx)].played;
+            } else {
+              // Black turn:
+              dtLastMove = game.moves[L-1-myIdx].played;
             }
           }
+          if (dtLastMove < game.chats[0].added)
+            document.getElementById("chatBtn").classList.add("somethingnew");
+        }
+        // Now that we used idx and played, re-format moves as for live games
+        game.moves = game.moves.map(m => m.squares);
+      }
+      if (gtype == "live" && game.clocks[0] < 0) {
+        // Game is unstarted. clocks and initime are ignored until move 2
+        game.clocks = [tc.mainTime, tc.mainTime];
+        game.initime = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
+        if (myIdx >= 0) {
+          // I play in this live game
+          GameStorage.update(game.id, {
+            clocks: game.clocks,
+            initime: game.initime
+          });
         }
-        if (!!game.rematchOffer) {
-          if (myIdx < 0) this.rematchOffer = "received";
+      }
+      // TODO: merge next 2 "if" conditions
+      if (!!game.drawOffer) {
+        if (game.drawOffer == "t")
+          // Three repetitions
+          this.drawOffer = "threerep";
+        else {
+          // Draw offered by any of the players:
+          if (myIdx < 0) this.drawOffer = "received";
           else {
             // I play in this game:
             if (
-              (game.rematchOffer == "w" && myIdx == 0) ||
-              (game.rematchOffer == "b" && myIdx == 1)
+              (game.drawOffer == "w" && myIdx == 0) ||
+              (game.drawOffer == "b" && myIdx == 1)
             )
-              this.rematchOffer = "sent";
-            else this.rematchOffer = "received";
+              this.drawOffer = "sent";
+            else this.drawOffer = "received";
           }
         }
-        this.repeat = {}; //reset: scan past moves' FEN:
-        let repIdx = 0;
-        let vr_tmp = new V(game.fenStart);
-        let curTurn = "n";
-        game.moves.forEach(m => {
-          playMove(m, vr_tmp);
-          const fenIdx = vr_tmp.getFen().replace(/ /g, "_");
-          this.repeat[fenIdx] = this.repeat[fenIdx]
-            ? this.repeat[fenIdx] + 1
-            : 1;
-        });
-        if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep";
-        this.game = Object.assign(
-          // NOTE: assign mycolor here, since BaseGame could also be VS computer
-          {
-            type: gtype,
-            increment: tc.increment,
-            mycolor: mycolor,
-            // opponent sid not strictly required (or available), but easier
-            // at least oppsid or oppid is available anyway:
-            oppsid: myIdx < 0 ? undefined : game.players[1 - myIdx].sid,
-            oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].id
-          },
-          game
-        );
-        this.$refs["basegame"].re_setVariables(this.game);
-        if (!this.gameIsLoading) {
-          // Initial loading:
-          this.gotMoveIdx = game.moves.length - 1;
-          // If we arrive here after 'nextGame' action, the board might be hidden
-          let boardDiv = document.querySelector(".game");
-          if (!!boardDiv && boardDiv.style.visibility == "hidden")
-            boardDiv.style.visibility = "visible";
+      }
+      if (!!game.rematchOffer) {
+        if (myIdx < 0) this.rematchOffer = "received";
+        else {
+          // I play in this game:
+          if (
+            (game.rematchOffer == "w" && myIdx == 0) ||
+            (game.rematchOffer == "b" && myIdx == 1)
+          )
+            this.rematchOffer = "sent";
+          else this.rematchOffer = "received";
         }
-        this.re_setClocks();
-        this.$nextTick(() => {
-          this.game.rendered = true;
-          // Did lastate arrive before game was rendered?
-          if (this.lastate) this.processLastate();
+      }
+      this.repeat = {}; //reset: scan past moves' FEN:
+      let repIdx = 0;
+      let vr_tmp = new V(game.fenStart);
+      let curTurn = "n";
+      game.moves.forEach(m => {
+        playMove(m, vr_tmp);
+        const fenIdx = vr_tmp.getFen().replace(/ /g, "_");
+        this.repeat[fenIdx] = this.repeat[fenIdx]
+          ? this.repeat[fenIdx] + 1
+          : 1;
+      });
+      if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep";
+      this.game = Object.assign(
+        // NOTE: assign mycolor here, since BaseGame could also be VS computer
+        {
+          type: gtype,
+          increment: tc.increment,
+          mycolor: mycolor,
+          // opponent sid not strictly required (or available), but easier
+          // at least oppsid or oppid is available anyway:
+          oppsid: myIdx < 0 ? undefined : game.players[1 - myIdx].sid,
+          oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].id
+        },
+        game
+      );
+      this.$refs["basegame"].re_setVariables(this.game);
+      if (!this.gameIsLoading) {
+        // Initial loading:
+        this.gotMoveIdx = game.moves.length - 1;
+        // If we arrive here after 'nextGame' action, the board might be hidden
+        let boardDiv = document.querySelector(".game");
+        if (!!boardDiv && boardDiv.style.visibility == "hidden")
+          boardDiv.style.visibility = "visible";
+      }
+      this.re_setClocks();
+      this.$nextTick(() => {
+        this.game.rendered = true;
+        // Did lastate arrive before game was rendered?
+        if (this.lastate) this.processLastate();
+      });
+      if (this.lastateAsked) {
+        this.lastateAsked = false;
+        this.sendLastate(game.oppsid);
+      }
+      if (this.gameIsLoading) {
+        this.gameIsLoading = false;
+        if (this.gotMoveIdx >= game.moves.length)
+          // Some moves arrived meanwhile...
+          this.askGameAgain();
+      }
+      if (!!callback) callback();
+    },
+    fetchGame: function(game, callback) {
+      const afterRetrieval = async (game) => {
+        await import("@/variants/" + game.vname + ".js")
+        .then((vModule) => {
+          window.V = vModule[game.vname + "Rules"];
+          this.loadGame(game, callback);
         });
-        if (this.lastateAsked) {
-          this.lastateAsked = false;
-          this.sendLastate(game.oppsid);
-        }
-        if (this.gameIsLoading) {
-          this.gameIsLoading = false;
-          if (this.gotMoveIdx >= game.moves.length)
-            // Some moves arrived meanwhile...
-            this.askGameAgain();
-        }
-        if (!!callback) callback();
       };
       if (!!game) {
         afterRetrieval(game);
         return;
       }
-      if (this.gameRef.rid) {
+      if (this.gameRef.rid)
         // Remote live game: forgetting about callback func... (TODO: design)
         this.send("askfullgame", { target: this.gameRef.rid });
-      } else {
+      else {
         // Local or corr game on server.
         // NOTE: afterRetrieval() is never called if game not found
         const gid = this.gameRef.id;
@@ -1074,11 +1079,10 @@ export default {
             {
               data: { gid: gid },
               success: (res) => {
-                let g = res.game;
-                g.moves.forEach(m => {
+                res.game.moves.forEach(m => {
                   m.squares = JSON.parse(m.squares);
                 });
-                afterRetrieval(g);
+                afterRetrieval(res.game);
               }
             }
           );
diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue
index 8f86f4c4..0840cd91 100644
--- a/client/src/views/Hall.vue
+++ b/client/src/views/Hall.vue
@@ -901,10 +901,13 @@ export default {
     },
     loadNewchallVariant: async function(cb) {
       const vname = this.getVname(this.newchallenge.vid);
-      const vModule = await import("@/variants/" + vname + ".js");
-      this.newchallenge.V = vModule.VariantRules;
-      this.newchallenge.vname = vname;
-      if (!!cb) cb();
+      await import("@/variants/" + vname + ".js")
+      .then((vModule) => {
+        window.V = vModule[vname + "Rules"];
+        this.newchallenge.V = window.V;
+        this.newchallenge.vname = vname;
+        if (!!cb) cb();
+      });
     },
     trySetNewchallDiag: function() {
       if (!this.newchallenge.fen) {
@@ -1101,22 +1104,24 @@ export default {
           return;
         }
         c.accepted = true;
-        const vModule = await import("@/variants/" + c.vname + ".js");
-        window.V = vModule.VariantRules;
-        if (!!c.to) {
-          // c.to == this.st.user.name (connected)
-          if (!!c.fen) {
-            const parsedFen = V.ParseFen(c.fen);
-            c.mycolor = V.GetOppCol(parsedFen.turn);
-            this.tchallDiag = getDiagram({
-              position: parsedFen.position,
-              orientation: c.mycolor
-            });
+        await import("@/variants/" + c.vname + ".js")
+        .then((vModule) => {
+          window.V = vModule[c.vname + "Rules"];
+          if (!!c.to) {
+            // c.to == this.st.user.name (connected)
+            if (!!c.fen) {
+              const parsedFen = V.ParseFen(c.fen);
+              c.mycolor = V.GetOppCol(parsedFen.turn);
+              this.tchallDiag = getDiagram({
+                position: parsedFen.position,
+                orientation: c.mycolor
+              });
+            }
+            this.curChallToAccept = c;
+            document.getElementById("modalAccept").checked = true;
           }
-          this.curChallToAccept = c;
-          document.getElementById("modalAccept").checked = true;
-        }
-        else this.finishProcessingChallenge(c);
+          else this.finishProcessingChallenge(c);
+        });
       }
       else {
         // My challenge
diff --git a/client/src/views/Problems.vue b/client/src/views/Problems.vue
index 2ea875e0..ae918b95 100644
--- a/client/src/views/Problems.vue
+++ b/client/src/views/Problems.vue
@@ -284,10 +284,12 @@ export default {
       // Condition: vid is a valid variant ID
       this.loadedVar = 0;
       const variant = this.st.variants.find(v => v.id == vid);
-      const vModule = await import("@/variants/" + variant.name + ".js");
-      window.V = vModule.VariantRules;
-      this.loadedVar = vid;
-      cb();
+      await import("@/variants/" + variant.name + ".js")
+      .then((vModule) => {
+        window.V = vModule[variant.name + "Rules"];
+        this.loadedVar = vid;
+        cb();
+      });
     },
     trySetDiagram: function(prob) {
       // Problem edit: FEN could be wrong or incomplete,
diff --git a/client/src/views/Rules.vue b/client/src/views/Rules.vue
index 844457d6..2af53b49 100644
--- a/client/src/views/Rules.vue
+++ b/client/src/views/Rules.vue
@@ -114,7 +114,7 @@ export default {
     re_setVariant: async function(vname) {
       await import("@/variants/" + vname + ".js")
       .then((vModule) => {
-        this.V = window.V = vModule.VariantRules;
+        this.V = window.V = vModule[vname + "Rules"];
         this.gameInfo.vname = vname;
       })
       .catch((err) => {
@@ -176,12 +176,13 @@ figure.diagram-container
   display: block
   .diagram
     display: block
-    width: 40%
+    width: 50%
     min-width: 240px
     margin-left: auto
     margin-right: auto
   .diag12
     float: left
+    width: 40%
     margin-left: calc(10% - 20px)
     margin-right: 40px
     @media screen and (max-width: 630px)
@@ -189,6 +190,7 @@ figure.diagram-container
       margin: 0 auto 10px auto
   .diag22
     float: left
+    width: 40%
     margin-right: calc(10% - 20px)
     @media screen and (max-width: 630px)
       float: none
-- 
2.44.0