From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 24 Feb 2020 00:24:14 +0000 (+0100)
Subject: Fix SuctionChess, draft HiddenRules (unfinished)
X-Git-Url: https://git.auder.net/variants/img/pieces/assets/config.php?a=commitdiff_plain;h=241bf8f2a9a2c48d793aeb0b1d20207f6371de70;p=vchess.git

Fix SuctionChess, draft HiddenRules (unfinished)
---

diff --git a/client/src/base_rules.js b/client/src/base_rules.js
index 7e4057bb..7949d7f6 100644
--- a/client/src/base_rules.js
+++ b/client/src/base_rules.js
@@ -53,11 +53,6 @@ export const ChessRules = class ChessRules {
     return "all";
   }
 
-  // Path to pieces
-  static getPpath(b) {
-    return b; //usual pieces in pieces/ folder
-  }
-
   // Turn "wb" into "B" (for FEN)
   static board2fen(b) {
     return b[0] == "w" ? b[1].toUpperCase() : b[1];
@@ -160,6 +155,11 @@ export const ChessRules = class ChessRules {
     return V.CoordToColumn(coords.y) + (V.size.x - coords.x);
   }
 
+  // Path to pieces
+  getPpath(b) {
+    return b; //usual pieces in pieces/ folder
+  }
+
   // Aggregates flags into one object
   aggregateFlags() {
     return this.castleFlags;
@@ -379,7 +379,9 @@ export const ChessRules = class ChessRules {
   // INITIALIZATION
 
   constructor(fen) {
-    this.re_init(fen);
+    // In printDiagram() fen isn't supply because only getPpath() is used
+    if (fen)
+      this.re_init(fen);
   }
 
   // Fen string fully describes the game state
diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue
index 62680021..d4423464 100644
--- a/client/src/components/Board.vue
+++ b/client/src/components/Board.vue
@@ -88,7 +88,7 @@ export default {
                   attrs: {
                     src:
                       "/images/pieces/" +
-                      V.getPpath(this.vr.board[ci][cj]) +
+                      this.vr.getPpath(this.vr.board[ci][cj], this.userColor, this.score) +
                       ".svg"
                   }
                 })
@@ -154,7 +154,7 @@ export default {
                 attrs: {
                   src:
                     "/images/pieces/" +
-                    this.vr.getReservePpath(playingColor, i) +
+                    this.vr.getReservePpath(i, playingColor) +
                     ".svg"
                 }
               }),
@@ -181,7 +181,7 @@ export default {
                 attrs: {
                   src:
                     "/images/pieces/" +
-                    this.vr.getReservePpath(oppCol, i) +
+                    this.vr.getReservePpath(i, oppCol) +
                     ".svg"
                 }
               }),
@@ -255,7 +255,7 @@ export default {
                 attrs: {
                   src:
                     "/images/pieces/" +
-                    V.getPpath(m.appear[0].c + m.appear[0].p) +
+                    this.vr.getPpath(m.appear[0].c + m.appear[0].p) +
                     ".svg"
                 },
                 class: { "choice-piece": true },
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index d1759ebc..920e19cb 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -148,5 +148,6 @@ export const translations = {
   "Reuse pieces": "Reuse pieces",
   "Reverse captures": "Reverse captures",
   "Shared pieces": "Shared pieces",
-  "Standard rules": "Standard rules"
+  "Standard rules": "Standard rules",
+  "Unidentified pieces": "Unidentified pieces"
 };
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index 3698845f..d3ead1a0 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -148,5 +148,6 @@ export const translations = {
   "Reuse pieces": "Reutilizar piezas",
   "Reverse captures": "Capturas invertidas",
   "Shared pieces": "Piezas compartidas",
-  "Standard rules": "Reglas estandar"
+  "Standard rules": "Reglas estandar",
+  "Unidentified pieces": "Piezas no identificadas"
 };
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 647c35e7..5abf762c 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -148,5 +148,6 @@ export const translations = {
   "Reuse pieces": "Réutiliser les pièces",
   "Reverse captures": "Captures inversées",
   "Shared pieces": "Pièces partagées",
-  "Standard rules": "Règles usuelles"
+  "Standard rules": "Règles usuelles",
+  "Unidentified pieces": "Pièces non identifiées"
 };
diff --git a/client/src/translations/rules/Hidden/en.pug b/client/src/translations/rules/Hidden/en.pug
new file mode 100644
index 00000000..f98808f9
--- /dev/null
+++ b/client/src/translations/rules/Hidden/en.pug
@@ -0,0 +1,26 @@
+p.boxed
+  | If a piece captures one of the same kind, both disappear.
+
+// TODO:
+  // Due to random 2 ranks: no castling (would reveal too much)
+  // To not reveal much, also no en passant
+
+p.
+  The defensive power of pawns is thus increased, because they don't fear
+  captures (by other pawns).
+
+p.
+  Endings are also affected quite a lot, and sometimes new threats occur:
+  on the diagram, 3.Bxg7 wins a pawn because 3...Bxg7 would make both
+  bishops disappear.
+
+figure.diagram-container
+  .diagram
+    | fen:r1bqkbnr/pp1ppppp/2n5/2p5/8/1P6/PBPPPPPP/RN1QKBNR:
+  figcaption After 1.b3 c5 2.Bb2 Nc6
+
+h3 Source
+
+p
+  a(href="https://www.chessvariants.com/rules/strate-go-chess") Strate-Go chess
+  | &nbsp;on chessvariants.com.
diff --git a/client/src/translations/rules/Hidden/es.pug b/client/src/translations/rules/Hidden/es.pug
new file mode 100644
index 00000000..b6adc808
--- /dev/null
+++ b/client/src/translations/rules/Hidden/es.pug
@@ -0,0 +1,23 @@
+p.boxed
+  | Si una pieza captura otra del mismo tipo, las dos desaparecen.
+
+p.
+  El poder defensivo de los peones aumenta así, ya que no temen
+  más capturas (por otros peones).
+
+p.
+  Las finales también se ven muy afectadas y, a veces, nuevas amenazas
+  ocurren: en el diagrama, 3.Bxg7 gana un peón porque 3...Bxg7 causaría
+  la desaparición de los dos alfiles.
+
+figure.diagram-container
+  .diagram
+    | fen:r1bqkbnr/pp1ppppp/2n5/2p5/8/1P6/PBPPPPPP/RN1QKBNR:
+  figcaption Después de 1.b3 c5 2.Bb2 Nc6
+
+h3 Fuente
+
+p
+  | La 
+  a(href="https://www.chessvariants.com/rules/antimatter-chess") variante Antimateria
+  | &nbsp;en chessvariants.com.
diff --git a/client/src/translations/rules/Hidden/fr.pug b/client/src/translations/rules/Hidden/fr.pug
new file mode 100644
index 00000000..7b1cdfae
--- /dev/null
+++ b/client/src/translations/rules/Hidden/fr.pug
@@ -0,0 +1,23 @@
+p.boxed
+  | Si une pièce en capture une autre du même type, les deux disparaissent.
+
+p.
+  Le pouvoir défensif des pions est ainsi augmenté, puisqu'ils ne craignent
+  plus les captures (par d'autres pions).
+
+p.
+  Les finales sont aussi beaucoup affectées, et parfois de nouvelles menaces
+  surviennent : sur le diagramme, 3.Bxg7 gagne un pion car 3...Bxg7 provoquerait
+  la disparition des deux fous.
+
+figure.diagram-container
+  .diagram
+    | fen:r1bqkbnr/pp1ppppp/2n5/2p5/8/1P6/PBPPPPPP/RN1QKBNR:
+  figcaption After 1.b3 c5 2.Bb2 Nc6
+
+h3 Source
+
+p
+  | La 
+  a(href="https://www.chessvariants.com/rules/antimatter-chess") variante Antimatière
+  | &nbsp;sur chessvariants.com.
diff --git a/client/src/translations/rules/Suction/en.pug b/client/src/translations/rules/Suction/en.pug
index 60ccf6ae..36ca0943 100644
--- a/client/src/translations/rules/Suction/en.pug
+++ b/client/src/translations/rules/Suction/en.pug
@@ -5,10 +5,14 @@ p.
   When a piece is taken it is not removed from the board, but moved instead
   to the square that was initially occupied by the capturing piece.
 
+p.
+  It is forbidden to "un-do" a capture right after it was played:
+  in the diagram position, 3...Bxg7 is not allowed.
+
 figure.diagram-container
   .diagram
-    | fen:rnbqkb1r/ppp1pppp/5B2/3p4/8/1P6/PnPPPPPP/RN1QKBNR:
-  figcaption After 1.b3 d5 2.Bb2 Nf6 3.Bxf6
+    | fen:rnbqk1nr/ppppppBp/6p1/8/8/1P6/PbPPPPPP/RN1QKBNR:
+  figcaption After 1.b3 g6 2.Bb2 Bg7 3.Bxg7
 
 ul
   li.
diff --git a/client/src/translations/rules/Suction/es.pug b/client/src/translations/rules/Suction/es.pug
index 685b7552..3bc9e275 100644
--- a/client/src/translations/rules/Suction/es.pug
+++ b/client/src/translations/rules/Suction/es.pug
@@ -6,10 +6,14 @@ p.
   Cuando se captura una pieza, no se retira del tablero, sino
   solo se movió a la casilla originalmente ocupada por la pieza que capturó.
 
+p.
+  Está prohibido "deshacer" una captura inmediatamente después de que se
+  haya jugado: en la posición del diagrama, 3...Bxg7 no está permitido.
+
 figure.diagram-container
   .diagram
-    | fen:rnbqkb1r/ppp1pppp/5B2/3p4/8/1P6/PnPPPPPP/RN1QKBNR:
-  figcaption Después de 1.b3 d5 2.Bb2 Nf6 3.Bxf6
+    | fen:rnbqk1nr/ppppppBp/6p1/8/8/1P6/PbPPPPPP/RN1QKBNR:
+  figcaption Después de 1.b3 g6 2.Bb2 Bg7 3.Bxg7
 
 ul
   li.
diff --git a/client/src/translations/rules/Suction/fr.pug b/client/src/translations/rules/Suction/fr.pug
index 2097062b..931517ef 100644
--- a/client/src/translations/rules/Suction/fr.pug
+++ b/client/src/translations/rules/Suction/fr.pug
@@ -6,10 +6,14 @@ p.
   Quand une pièce est capturée elle n'est pas retirée de l'échiquier, mais
   seulement déplacée sur la case initialement occupée par la pièce capturante.
 
+p.
+  Il est interdit de "défaire" une capture juste après qu'elle a été jouée :
+  dans la position du diagramme, 3...Bxg7 n'est pas autorisé.
+
 figure.diagram-container
   .diagram
-    | fen:rnbqkb1r/ppp1pppp/5B2/3p4/8/1P6/PnPPPPPP/RN1QKBNR:
-  figcaption Après 1.b3 d5 2.Bb2 Nf6 3.Bxf6
+    | fen:rnbqk1nr/ppppppBp/6p1/8/8/1P6/PbPPPPPP/RN1QKBNR:
+  figcaption Après 1.b3 g6 2.Bb2 Bg7 3.Bxg7
 
 ul
   li.
diff --git a/client/src/utils/printDiagram.js b/client/src/utils/printDiagram.js
index aebdc247..d4562bf2 100644
--- a/client/src/utils/printDiagram.js
+++ b/client/src/utils/printDiagram.js
@@ -70,6 +70,7 @@ export function getDiagram(args) {
   const orientation = args.orientation || "w";
   const markArray = getMarkArray(args.marks);
   const shadowArray = getShadowArray(args.shadow);
+  const vr = new V(); //just for pieces images paths
   let boardDiv = "";
   const [startX, startY, inc] =
     orientation == "w" ? [0, 0, 1] : [V.size.x - 1, V.size.y - 1, -1];
@@ -87,7 +88,7 @@ export function getDiagram(args) {
         boardDiv +=
           "<img " +
           "src='/images/pieces/" +
-          V.getPpath(board[i][j]) +
+          vr.getPpath(board[i][j]) +
           ".svg' " +
           "class='piece'/>";
       }
diff --git a/client/src/variants/Alice.js b/client/src/variants/Alice.js
index e7daf7f8..b16d728e 100644
--- a/client/src/variants/Alice.js
+++ b/client/src/variants/Alice.js
@@ -25,14 +25,14 @@ export const VariantRules = class AliceRules extends ChessRules {
     };
   }
 
-  static getPpath(b) {
-    return (Object.keys(this.ALICE_PIECES).includes(b[1]) ? "Alice/" : "") + b;
-  }
-
   static get PIECES() {
     return ChessRules.PIECES.concat(Object.keys(V.ALICE_PIECES));
   }
 
+  getPpath(b) {
+    return (Object.keys(this.ALICE_PIECES).includes(b[1]) ? "Alice/" : "") + b;
+  }
+
   setOtherVariables(fen) {
     super.setOtherVariables(fen);
     const rows = V.ParseFen(fen).position.split("/");
diff --git a/client/src/variants/Antiking.js b/client/src/variants/Antiking.js
index 3b1b36e3..f07f6268 100644
--- a/client/src/variants/Antiking.js
+++ b/client/src/variants/Antiking.js
@@ -3,10 +3,6 @@ import { ArrayFun } from "@/utils/array";
 import { randInt } from "@/utils/alea";
 
 export const VariantRules = class AntikingRules extends ChessRules {
-  static getPpath(b) {
-    return b[1] == "a" ? "Antiking/" + b : b;
-  }
-
   static get ANTIKING() {
     return "a";
   }
@@ -15,6 +11,10 @@ export const VariantRules = class AntikingRules extends ChessRules {
     return ChessRules.PIECES.concat([V.ANTIKING]);
   }
 
+  getPpath(b) {
+    return b[1] == "a" ? "Antiking/" + b : b;
+  }
+
   setOtherVariables(fen) {
     super.setOtherVariables(fen);
     this.antikingPos = { w: [-1, -1], b: [-1, -1] };
diff --git a/client/src/variants/Baroque.js b/client/src/variants/Baroque.js
index 6557b380..8f1f9ba0 100644
--- a/client/src/variants/Baroque.js
+++ b/client/src/variants/Baroque.js
@@ -11,17 +11,17 @@ export const VariantRules = class BaroqueRules extends ChessRules {
     return false;
   }
 
-  static getPpath(b) {
+  static get PIECES() {
+    return ChessRules.PIECES.concat([V.IMMOBILIZER]);
+  }
+
+  getPpath(b) {
     if (b[1] == "m")
       //'m' for Immobilizer (I is too similar to 1)
       return "Baroque/" + b;
     return b; //usual piece
   }
 
-  static get PIECES() {
-    return ChessRules.PIECES.concat([V.IMMOBILIZER]);
-  }
-
   // No castling, but checks, so keep track of kings
   setOtherVariables(fen) {
     this.kingPos = { w: [-1, -1], b: [-1, -1] };
diff --git a/client/src/variants/Checkered.js b/client/src/variants/Checkered.js
index 0e4f0a0d..d477a6d0 100644
--- a/client/src/variants/Checkered.js
+++ b/client/src/variants/Checkered.js
@@ -1,10 +1,6 @@
 import { ChessRules } from "@/base_rules";
 
 export const VariantRules = class CheckeredRules extends ChessRules {
-  static getPpath(b) {
-    return b[0] == "c" ? "Checkered/" + b : b;
-  }
-
   static board2fen(b) {
     const checkered_codes = {
       p: "s",
@@ -40,6 +36,10 @@ export const VariantRules = class CheckeredRules extends ChessRules {
     return ChessRules.PIECES.concat(["s", "t", "u", "c", "o"]);
   }
 
+  getPpath(b) {
+    return b[0] == "c" ? "Checkered/" + b : b;
+  }
+
   setOtherVariables(fen) {
     super.setOtherVariables(fen);
     // Local stack of non-capturing checkered moves:
@@ -130,7 +130,7 @@ export const VariantRules = class CheckeredRules extends ChessRules {
         }
       }
       if (m.vanish.length == 1) moves.push(m);
-      //no capture
+      // No capture
       else {
         // A capture occured (m.vanish.length == 2)
         m.appear[0].c = "c";
@@ -156,7 +156,7 @@ export const VariantRules = class CheckeredRules extends ChessRules {
   // Does m2 un-do m1 ? (to disallow undoing checkered moves)
   oppositeMoves(m1, m2) {
     return (
-      !!m1 &&
+      m1 &&
       m2.appear[0].c == "c" &&
       m2.appear.length == 1 &&
       m2.vanish.length == 1 &&
@@ -170,8 +170,8 @@ export const VariantRules = class CheckeredRules extends ChessRules {
   filterValid(moves) {
     if (moves.length == 0) return [];
     const color = this.turn;
+    const L = this.cmoves.length; //at least 1: init from FEN
     return moves.filter(m => {
-      const L = this.cmoves.length; //at least 1: init from FEN
       if (this.oppositeMoves(this.cmoves[L - 1], m)) return false;
       this.play(m);
       const res = !this.underCheck(color);
diff --git a/client/src/variants/Crazyhouse.js b/client/src/variants/Crazyhouse.js
index d7d82b8c..24018d14 100644
--- a/client/src/variants/Crazyhouse.js
+++ b/client/src/variants/Crazyhouse.js
@@ -104,7 +104,7 @@ export const VariantRules = class CrazyhouseRules extends ChessRules {
   }
 
   // Used by the interface:
-  getReservePpath(color, index) {
+  getReservePpath(index, color) {
     return color + V.RESERVE_PIECES[index];
   }
 
diff --git a/client/src/variants/Dark.js b/client/src/variants/Dark.js
index d6515efd..3944ca16 100644
--- a/client/src/variants/Dark.js
+++ b/client/src/variants/Dark.js
@@ -46,9 +46,7 @@ export const VariantRules = class DarkRules extends ChessRules {
                 V.OnBoard(i + pawnShift[color], j + shiftY) &&
                 this.board[i + pawnShift[color]][j + shiftY] == V.EMPTY
               ) {
-                this.enlightened[color][i + pawnShift[color]][
-                  j + shiftY
-                ] = true;
+                this.enlightened[color][i + pawnShift[color]][j + shiftY] = true;
               }
             }
           }
@@ -83,25 +81,15 @@ export const VariantRules = class DarkRules extends ChessRules {
     return potentialMoves; //because there are no checks
   }
 
-  atLeastOneMove() {
-    if (this.kingPos[this.turn][0] < 0) return false;
-    return true; //TODO: is it right?
-  }
-
-  underCheck() {
-    return false; //there is no check
-  }
-
   getCheckSquares() {
     return [];
   }
 
   updateVariables(move) {
     super.updateVariables(move);
-    if (move.vanish.length >= 2 && move.vanish[1].p == V.KING) {
-      // We took opponent king ! (because if castle vanish[1] is a rook)
+    if (move.vanish.length >= 2 && move.vanish[1].p == V.KING)
+      // We took opponent king (because if castle vanish[1] is a rook)
       this.kingPos[this.turn] = [-1, -1];
-    }
 
     // Update lights for both colors:
     this.updateEnlightened();
@@ -111,15 +99,9 @@ export const VariantRules = class DarkRules extends ChessRules {
     super.unupdateVariables(move);
     const c = move.vanish[0].c;
     const oppCol = V.GetOppCol(c);
-    if (this.kingPos[oppCol][0] < 0) {
-      // Last move took opponent's king
-      for (let psq of move.vanish) {
-        if (psq.p == "k") {
-          this.kingPos[oppCol] = [psq.x, psq.y];
-          break;
-        }
-      }
-    }
+    if (this.kingPos[oppCol][0] < 0)
+      // Last move took opponent's king:
+      this.kingPos[oppCol] = [move.vanish[1].x, move.vanish[1].y];
 
     // Update lights for both colors:
     this.updateEnlightened();
@@ -129,12 +111,10 @@ export const VariantRules = class DarkRules extends ChessRules {
     const color = this.turn;
     const kp = this.kingPos[color];
     if (kp[0] < 0)
-      //king disappeared
+      // King disappeared
       return color == "w" ? "0-1" : "1-0";
-    if (this.atLeastOneMove())
-      // game not over
-      return "*";
-    return "1/2"; //no moves but kings still there (seems impossible)
+    // Assume that stalemate is impossible (I think so. Would need proof...)
+    return "*";
   }
 
   static get THRESHOLD_MATE() {
diff --git a/client/src/variants/Grand.js b/client/src/variants/Grand.js
index c4e2eb1d..d804f614 100644
--- a/client/src/variants/Grand.js
+++ b/client/src/variants/Grand.js
@@ -5,10 +5,6 @@ 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 {
-  static getPpath(b) {
-    return ([V.MARSHALL, V.CARDINAL].includes(b[1]) ? "Grand/" : "") + b;
-  }
-
   static IsGoodFen(fen) {
     if (!ChessRules.IsGoodFen(fen)) return false;
     const fenParsed = V.ParseFen(fen);
@@ -35,6 +31,10 @@ export const VariantRules = class GrandRules extends ChessRules {
     return Object.assign(ChessRules.ParseFen(fen), { captured: fenParts[5] });
   }
 
+  getPpath(b) {
+    return ([V.MARSHALL, V.CARDINAL].includes(b[1]) ? "Grand/" : "") + b;
+  }
+
   getFen() {
     return super.getFen() + " " + this.getCapturedFen();
   }
diff --git a/client/src/variants/Hidden.js b/client/src/variants/Hidden.js
new file mode 100644
index 00000000..1836352a
--- /dev/null
+++ b/client/src/variants/Hidden.js
@@ -0,0 +1,185 @@
+import { ChessRules } from "@/base_rules";
+import { ArrayFun } from "@/utils/array";
+import { randInt } from "@/utils/alea";
+
+export const VariantRules = class HiddenRules extends ChessRules {
+  static get HasFlags() {
+    return false;
+  }
+
+  static get HasEnpassant() {
+    return false;
+  }
+
+  // Analyse in Hidden mode makes no sense
+  static get CanAnalyze() {
+    return false;
+  }
+
+  // Moves are revealed only when game ends
+  static get ShowMoves() {
+    return "none";
+  }
+
+  static get HIDDEN_DECODE() {
+    return {
+      s: "p",
+      t: "q",
+      u: "r",
+      c: "b",
+      o: "n",
+      l: "k"
+    };
+  }
+  static get HIDDEN_CODE() {
+    return {
+      p: "s",
+      q: "t",
+      r: "u",
+      b: "c",
+      n: "o",
+      k: "l"
+    };
+  }
+
+  static get PIECES() {
+    return ChessRules.PIECES.concat(Object.values(V.HIDDEN_CODE));
+  }
+
+  // Scan board for kings positions (no castling)
+  scanKingsRooks(fen) {
+    this.kingPos = { w: [-1, -1], b: [-1, -1] };
+    const fenRows = V.ParseFen(fen).position.split("/");
+    for (let i = 0; i < fenRows.length; i++) {
+      let k = 0; //column index on board
+      for (let j = 0; j < fenRows[i].length; j++) {
+        switch (fenRows[i].charAt(j)) {
+          case "k":
+          case "l":
+            this.kingPos["b"] = [i, k];
+            break;
+          case "K":
+          case "L":
+            this.kingPos["w"] = [i, k];
+            break;
+          default: {
+            const num = parseInt(fenRows[i].charAt(j));
+            if (!isNaN(num)) k += num - 1;
+          }
+        }
+        k++;
+      }
+    }
+  }
+
+  getPpath(b, color, score) {
+    if (Object.keys(V.HIDDEN_DECODE).includes(b[1])) {
+      // Supposed to be hidden.
+      if (score == "*" && (!color || color != b[0]))
+        return "Hidden/" + b[0] + "p";
+      // Else: condition OK to show the piece
+      return b[0] + V.HIDDEN_DECODE[b[1]];
+    }
+    // The piece is already not supposed to be hidden:
+    return b;
+  }
+
+  //getPotentialMovesFrom: TODO: write
+
+  // TODO: bishops on different colors, a1 --> h1, h2 --> a2 ?
+  static GenRandInitFen() {
+    let pieces = { w: new Array(8), b: new Array(8) };
+    // Shuffle pieces + pawns on two first ranks
+    for (let c of ["w", "b"]) {
+      let positions = ArrayFun.range(16);
+
+      // Get random squares for bishops
+      let randIndex = 2 * randInt(8);
+      const bishop1Pos = positions[randIndex];
+      // The second bishop must be on a square of different color
+      let randIndex_tmp = 2 * randInt(8) + 1;
+      const bishop2Pos = positions[randIndex_tmp];
+      // Remove chosen squares
+      positions.splice(Math.max(randIndex, randIndex_tmp), 1);
+      positions.splice(Math.min(randIndex, randIndex_tmp), 1);
+
+      // Get random squares for knights
+      randIndex = randInt(14);
+      const knight1Pos = positions[randIndex];
+      positions.splice(randIndex, 1);
+      randIndex = randInt(13);
+      const knight2Pos = positions[randIndex];
+      positions.splice(randIndex, 1);
+
+      // Get random square for queen
+      randIndex = randInt(12);
+      const queenPos = positions[randIndex];
+      positions.splice(randIndex, 1);
+
+      // Get random squares for pawns
+      // TODO...
+
+      // 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";
+    }
+    return (
+      pieces["b"].join("") +
+      "/pppppppp/8/8/8/8/PPPPPPPP/" +
+      pieces["w"].join("").toUpperCase() +
+      " w 0"
+    );
+  }
+
+  getCheckSquares() {
+    return [];
+  }
+
+  updateVariables(move) {
+    super.updateVariables(move);
+    if (
+      move.vanish.length >= 2 &&
+      [V.KING,V.HIDDEN_CODE[V.KING]].includes(move.vanish[1].p)
+    ) {
+      // We took opponent king
+      this.kingPos[this.turn] = [-1, -1];
+    }
+  }
+
+  unupdateVariables(move) {
+    super.unupdateVariables(move);
+    const c = move.vanish[0].c;
+    const oppCol = V.GetOppCol(c);
+    if (this.kingPos[oppCol][0] < 0)
+      // Last move took opponent's king:
+      this.kingPos[oppCol] = [move.vanish[1].x, move.vanish[1].y];
+  }
+
+  getCurrentScore() {
+    const color = this.turn;
+    const kp = this.kingPos[color];
+    if (kp[0] < 0)
+      // King disappeared
+      return color == "w" ? "0-1" : "1-0";
+    // Assume that stalemate is impossible:
+    return "*";
+  }
+
+  getComputerMove() {
+    // Just return a random move. TODO: something smarter...
+    const moves = this.getAllValidMoves();
+    return moves[randInt(moves.length)];
+  }
+};
diff --git a/client/src/variants/Recycle.js b/client/src/variants/Recycle.js
index d99228cd..50ccd74e 100644
--- a/client/src/variants/Recycle.js
+++ b/client/src/variants/Recycle.js
@@ -74,7 +74,7 @@ export const VariantRules = class RecycleRules extends ChessRules {
   }
 
   // Used by the interface:
-  getReservePpath(color, index) {
+  getReservePpath(index, color) {
     return color + V.RESERVE_PIECES[index];
   }
 
diff --git a/client/src/variants/Suction.js b/client/src/variants/Suction.js
index bfb9fa00..38ee5c2e 100644
--- a/client/src/variants/Suction.js
+++ b/client/src/variants/Suction.js
@@ -1,6 +1,35 @@
 import { ChessRules, PiPo, Move } from "@/base_rules";
 
-export const VariantRules = class AntimatterRules extends ChessRules {
+export const VariantRules = class SuctionRules extends ChessRules {
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    // Local stack of captures
+    this.cmoves = [];
+    const cmove = fen.split(" ")[5];
+    if (cmove == "-") this.cmoves.push(null);
+    else {
+      this.cmoves.push({
+        start: ChessRules.SquareToCoords(cmove.substr(0, 2)),
+        end: ChessRules.SquareToCoords(cmove.substr(2))
+      });
+    }
+  }
+
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const fenParts = fen.split(" ");
+    if (fenParts.length != 6) return false;
+    if (fenParts[5] != "-" && !fenParts[5].match(/^([a-h][1-8]){2}$/))
+      return false;
+    return true;
+  }
+
+  getCmove(move) {
+    if (move.vanish.length == 2)
+      return { start: move.start, end: move.end };
+    return null;
+  }
+
   getBasicMove([sx, sy], [ex, ey]) {
     const startColor = this.getColor(sx, sy);
     const startPiece = this.getPiece(sx, sy);
@@ -121,6 +150,27 @@ export const VariantRules = class AntimatterRules extends ChessRules {
     return [];
   }
 
+  // Does m2 un-do m1 ? (to disallow undoing captures)
+  oppositeMoves(m1, m2) {
+    return (
+      m1 &&
+      m2.vanish.length == 2 &&
+      m1.start.x == m2.start.x &&
+      m1.end.x == m2.end.x &&
+      m1.start.y == m2.start.y &&
+      m1.end.y == m2.end.y
+    );
+  }
+
+  filterValid(moves) {
+    if (moves.length == 0) return [];
+    const color = this.turn;
+    return moves.filter(m => {
+      const L = this.cmoves.length; //at least 1: init from FEN
+      return !this.oppositeMoves(this.cmoves[L - 1], m);
+    });
+  }
+
   updateVariables(move) {
     super.updateVariables(move);
     if (move.vanish.length == 2) {
@@ -139,12 +189,32 @@ export const VariantRules = class AntimatterRules extends ChessRules {
     }
   }
 
-  atLeastOneMove() {
-    return true;
+  static GenRandInitFen() {
+    // Add empty cmove:
+    return ChessRules.GenRandInitFen() + " -";
   }
 
-  filterValid(moves) {
-    return moves;
+  getFen() {
+    const L = this.cmoves.length;
+    const cmoveFen = !this.cmoves[L - 1]
+      ? "-"
+      : ChessRules.CoordsToSquare(this.cmoves[L - 1].start) +
+        ChessRules.CoordsToSquare(this.cmoves[L - 1].end);
+    return super.getFen() + " " + cmoveFen;
+  }
+
+  play(move) {
+    this.cmoves.push(this.getCmove(move));
+    super.play(move);
+  }
+
+  undo(move) {
+    this.cmoves.pop();
+    super.undo(move);
+  }
+
+  atLeastOneMove() {
+    return true;
   }
 
   getCheckSquares() {
diff --git a/client/src/variants/Wildebeest.js b/client/src/variants/Wildebeest.js
index 933abbde..27b131d5 100644
--- a/client/src/variants/Wildebeest.js
+++ b/client/src/variants/Wildebeest.js
@@ -3,10 +3,6 @@ import { ArrayFun } from "@/utils/array";
 import { sample, randInt } from "@/utils/alea";
 
 export const VariantRules = class WildebeestRules extends ChessRules {
-  static getPpath(b) {
-    return ([V.CAMEL, V.WILDEBEEST].includes(b[1]) ? "Wildebeest/" : "") + b;
-  }
-
   static get size() {
     return { x: 10, y: 11 };
   }
@@ -52,6 +48,10 @@ export const VariantRules = class WildebeestRules extends ChessRules {
     return true;
   }
 
+  getPpath(b) {
+    return ([V.CAMEL, V.WILDEBEEST].includes(b[1]) ? "Wildebeest/" : "") + b;
+  }
+
   // There may be 2 enPassant squares (if pawn jump 3 squares)
   getEnpassantFen() {
     const L = this.epSquares.length;
diff --git a/server/db/populate.sql b/server/db/populate.sql
index 2876ba78..384b7356 100644
--- a/server/db/populate.sql
+++ b/server/db/populate.sql
@@ -16,6 +16,7 @@ insert or ignore into Variants (name,description) values
   ('Enpassant', 'Capture en passant'),
   ('Extinction', 'Capture all of a kind'),
   ('Grand', 'Big board'),
+  ('Hidden', 'Unidentified pieces'),
   ('Losers', 'Lose all pieces'),
   ('Magnetic', 'Laws of attraction'),
   ('Marseille', 'Move twice'),