From 6e0f28425075e6d2d79cab6d30bca6ce6d55f19d Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Sun, 17 May 2020 23:01:05 +0200
Subject: [PATCH] Add Koopa chess, fix Apocalypse and Dice variants

---
 client/public/images/pieces/Koopa/bc.svg   |   1 +
 client/public/images/pieces/Koopa/bl.svg   |   1 +
 client/public/images/pieces/Koopa/bo.svg   |   1 +
 client/public/images/pieces/Koopa/bs.svg   |   1 +
 client/public/images/pieces/Koopa/bt.svg   |   1 +
 client/public/images/pieces/Koopa/bu.svg   |   1 +
 client/public/images/pieces/Koopa/wc.svg   |   1 +
 client/public/images/pieces/Koopa/wl.svg   |   1 +
 client/public/images/pieces/Koopa/wo.svg   |   1 +
 client/public/images/pieces/Koopa/ws.svg   |   1 +
 client/public/images/pieces/Koopa/wt.svg   |   1 +
 client/public/images/pieces/Koopa/wu.svg   |   1 +
 client/src/components/Board.vue            |   3 +
 client/src/translations/rules/Dice/en.pug  |   6 +-
 client/src/translations/rules/Dice/es.pug  |   6 +-
 client/src/translations/rules/Dice/fr.pug  |   6 +-
 client/src/translations/rules/Koopa/en.pug |  54 +++-
 client/src/translations/rules/Koopa/es.pug |  54 ++++
 client/src/translations/rules/Koopa/fr.pug |  54 ++++
 client/src/utils/array.js                  |   2 +-
 client/src/variants/Alice.js               |   3 +-
 client/src/variants/Apocalypse.js          |  19 +-
 client/src/variants/Dice.js                | 229 ++++++++--------
 client/src/variants/Grand.js               |  26 +-
 client/src/variants/Koopa.js               | 291 ++++++++++++++++++---
 client/src/views/Faq.vue                   |   1 +
 client/src/views/MyGames.vue               |   2 +-
 server/models/User.js                      |   2 +-
 28 files changed, 580 insertions(+), 190 deletions(-)
 create mode 120000 client/public/images/pieces/Koopa/bc.svg
 create mode 120000 client/public/images/pieces/Koopa/bl.svg
 create mode 120000 client/public/images/pieces/Koopa/bo.svg
 create mode 120000 client/public/images/pieces/Koopa/bs.svg
 create mode 120000 client/public/images/pieces/Koopa/bt.svg
 create mode 120000 client/public/images/pieces/Koopa/bu.svg
 create mode 120000 client/public/images/pieces/Koopa/wc.svg
 create mode 120000 client/public/images/pieces/Koopa/wl.svg
 create mode 120000 client/public/images/pieces/Koopa/wo.svg
 create mode 120000 client/public/images/pieces/Koopa/ws.svg
 create mode 120000 client/public/images/pieces/Koopa/wt.svg
 create mode 120000 client/public/images/pieces/Koopa/wu.svg

diff --git a/client/public/images/pieces/Koopa/bc.svg b/client/public/images/pieces/Koopa/bc.svg
new file mode 120000
index 00000000..b30a26ad
--- /dev/null
+++ b/client/public/images/pieces/Koopa/bc.svg
@@ -0,0 +1 @@
+../Alice/bc.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Koopa/bl.svg b/client/public/images/pieces/Koopa/bl.svg
new file mode 120000
index 00000000..a10d9e0a
--- /dev/null
+++ b/client/public/images/pieces/Koopa/bl.svg
@@ -0,0 +1 @@
+../Alice/bl.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Koopa/bo.svg b/client/public/images/pieces/Koopa/bo.svg
new file mode 120000
index 00000000..1200186b
--- /dev/null
+++ b/client/public/images/pieces/Koopa/bo.svg
@@ -0,0 +1 @@
+../Alice/bo.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Koopa/bs.svg b/client/public/images/pieces/Koopa/bs.svg
new file mode 120000
index 00000000..e8cf23a8
--- /dev/null
+++ b/client/public/images/pieces/Koopa/bs.svg
@@ -0,0 +1 @@
+../Alice/bs.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Koopa/bt.svg b/client/public/images/pieces/Koopa/bt.svg
new file mode 120000
index 00000000..c517549b
--- /dev/null
+++ b/client/public/images/pieces/Koopa/bt.svg
@@ -0,0 +1 @@
+../Alice/bt.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Koopa/bu.svg b/client/public/images/pieces/Koopa/bu.svg
new file mode 120000
index 00000000..09e6ea3e
--- /dev/null
+++ b/client/public/images/pieces/Koopa/bu.svg
@@ -0,0 +1 @@
+../Alice/bu.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Koopa/wc.svg b/client/public/images/pieces/Koopa/wc.svg
new file mode 120000
index 00000000..d23af91d
--- /dev/null
+++ b/client/public/images/pieces/Koopa/wc.svg
@@ -0,0 +1 @@
+../Alice/wc.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Koopa/wl.svg b/client/public/images/pieces/Koopa/wl.svg
new file mode 120000
index 00000000..51c1893a
--- /dev/null
+++ b/client/public/images/pieces/Koopa/wl.svg
@@ -0,0 +1 @@
+../Alice/wl.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Koopa/wo.svg b/client/public/images/pieces/Koopa/wo.svg
new file mode 120000
index 00000000..4a85712d
--- /dev/null
+++ b/client/public/images/pieces/Koopa/wo.svg
@@ -0,0 +1 @@
+../Alice/wo.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Koopa/ws.svg b/client/public/images/pieces/Koopa/ws.svg
new file mode 120000
index 00000000..659b2de0
--- /dev/null
+++ b/client/public/images/pieces/Koopa/ws.svg
@@ -0,0 +1 @@
+../Alice/ws.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Koopa/wt.svg b/client/public/images/pieces/Koopa/wt.svg
new file mode 120000
index 00000000..447fc4fe
--- /dev/null
+++ b/client/public/images/pieces/Koopa/wt.svg
@@ -0,0 +1 @@
+../Alice/wt.svg
\ No newline at end of file
diff --git a/client/public/images/pieces/Koopa/wu.svg b/client/public/images/pieces/Koopa/wu.svg
new file mode 120000
index 00000000..c1403b33
--- /dev/null
+++ b/client/public/images/pieces/Koopa/wu.svg
@@ -0,0 +1 @@
+../Alice/wu.svg
\ No newline at end of file
diff --git a/client/src/components/Board.vue b/client/src/components/Board.vue
index a83be0cc..5ed351a7 100644
--- a/client/src/components/Board.vue
+++ b/client/src/components/Board.vue
@@ -64,6 +64,9 @@ export default {
           lmHighlights[m.start.x + sizeX * m.start.y] = true;
         if (!m.end.noHighlight && V.OnBoard(m.end.x, m.end.y))
           lmHighlights[m.end.x + sizeX * m.end.y] = true;
+        if (!!m.start.toplay)
+          // For Dice variant (at least?)
+          lmHighlights[m.start.toplay[0] + sizeX * m.start.toplay[1]] = true;
       });
     }
     const showLight = (
diff --git a/client/src/translations/rules/Dice/en.pug b/client/src/translations/rules/Dice/en.pug
index 91be2d1a..f2f51a19 100644
--- a/client/src/translations/rules/Dice/en.pug
+++ b/client/src/translations/rules/Dice/en.pug
@@ -2,9 +2,9 @@ p.boxed.
   Play the piece type determined by a dice roll.
 
 p.
-  At each turn, click on any empty square first: you will see a piece type
-  written in the moves list, and a piece of this nature will be highlighted on
-  the board. You then have to play a move with this piece's type.
+  The first white move is chosen freely. Then, at each turn, you must play a
+  piece of the highlighted type on the board.
+  The piece's type to play is indicated in the moves list as well.
 
 p There is no check or checkmate: the goal is to capture the king.
 
diff --git a/client/src/translations/rules/Dice/es.pug b/client/src/translations/rules/Dice/es.pug
index 1799c531..ca70e8a2 100644
--- a/client/src/translations/rules/Dice/es.pug
+++ b/client/src/translations/rules/Dice/es.pug
@@ -2,9 +2,9 @@ p.boxed.
   Juega la pieza cuyo tipo está determinado por una tirada de dado.
 
 p.
-  En cada turno, haga clic en una casilla vacía: verá un tipo de pieza
-  escrito en la lista de jugadas, y se indicará una pieza de esta naturaleza
-  en el tablero. Luego debes jugar un movimiento con una pieza de este tipo.
+  El primer movimiento blanco se elige libremente. Luego, en cada ronda, usted
+  debe jugar una pieza del tipo resaltado en el tablero de ajedrez.
+  El tipo de pieza a jugar también se indica en la lista de movimientos.
 
 p No hay jaque ni jaque mate: el objetivo es capturar al rey.
 
diff --git a/client/src/translations/rules/Dice/fr.pug b/client/src/translations/rules/Dice/fr.pug
index a5d852f5..d637792b 100644
--- a/client/src/translations/rules/Dice/fr.pug
+++ b/client/src/translations/rules/Dice/fr.pug
@@ -2,9 +2,9 @@ p.boxed.
   Jouez la pièce dont le type est déterminé par un lancer de dé.
 
 p.
-  À chaque tour, cliquez sur une case vide : vous verrez un type de pièce
-  écrit dans la liste des coups, et une pièce de cette nature sera indiquée
-  sur l'échiquier. Vous devez ensuite jouer un coup avec une pièce de ce type.
+  Le premier coup blanc est choisi librement. Ensuite, à chaque tour, vous
+  devez jouer une pièce du type mis en valeur sur l'échiquier.
+  Le type de pièce à jouer est également indiqué dans la liste des coups.
 
 p Il n'y a ni échec ni mat : l'objectif est de capturer le roi.
 
diff --git a/client/src/translations/rules/Koopa/en.pug b/client/src/translations/rules/Koopa/en.pug
index 25cdf1d7..ae7169b8 100644
--- a/client/src/translations/rules/Koopa/en.pug
+++ b/client/src/translations/rules/Koopa/en.pug
@@ -1,3 +1,51 @@
-// TODO
-// Pritchard middle page 45, section 3.6
-//https://www.chessvariants.com/crossover.dir/koopachess.html
+p.boxed
+  | Captured pieces are stunned. They disappear if you capture them again.
+
+p
+  | This variant is inspired by the 
+  a(href="https://en.wikipedia.org/wiki/Super_Mario") Super Mario
+  | &nbsp;universe.
+  | When a piece captures another, it "bounce" on it until the next square in
+  | the movement's direction. If this next square is occupied, then it keeps
+  | bouncing and reach the following square, until either a free square or the
+  | edge of the board is met. In this last case, the capturer is lost.
+
+p.
+  Pieces bounced over are then "stunned" for two moves (four half-moves):
+  they cannot move during this period. If they get captured again while being
+  stunned, they are "kicked" out of the board, and all pieces standing on their
+  way vanish as well.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:nrkqnrbb/ppp2ppp/3p4/4p3/1P3P2/8/P1PPP1PP/BNQRNBKR:
+  .diagram.diag22
+    | fen:nukqnrbb/pps2ppp/3s4/4s3/1P6/8/P1PPP1PP/BNQRNBKR:
+  figcaption Before and after 1.fxe5
+
+p.
+  After the move 1.fxe5 on the diagram, the red pieces are stunned for two
+  moves. So white can then play 2.Bxe5, kicking the pawn and capturing (for
+  real) at least the h8 bishop. An option for black may be 1...Qg5, such
+  that after 2.Bxe5 Qxe5 black threaten to kick the white stunned bishop out.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:nuk1nrb1/pps2p1p/3s4/4B1q1/1P6/8/P1PPP1PP/1NQRNBKR:
+  .diagram.diag22
+    | fen:nuk1nrb1/pps2p1p/3s4/3qC3/1P6/8/P1PPP1PP/1NQRNBKR:
+  figcaption After 1...Qg5 2.Bxe5 Qxe5: white bishop is stunned.
+
+p.
+  The goal is to capture the enemy king. Moves which kick your own king
+  out are forbidden, but stunning him is allowed.
+
+h3 Source
+
+p
+  a(href="https://www.jsbeasley.co.uk/encyc.htm")
+    | The Classified Encyclopedia of Chess Variants
+  | , section 3.6. This variant is also listed on chessvariants.com: 
+  a(href="https://www.chessvariants.com/crossover.dir/koopachess.html")
+    | Koopa chess
+  | .
diff --git a/client/src/translations/rules/Koopa/es.pug b/client/src/translations/rules/Koopa/es.pug
index e69de29b..e02cdfd2 100644
--- a/client/src/translations/rules/Koopa/es.pug
+++ b/client/src/translations/rules/Koopa/es.pug
@@ -0,0 +1,54 @@
+p.boxed
+  | Las piezas capturadas quedan aturdidas.
+  | Desaparecerán si los captura de nuevo.
+
+p
+  | Esta variante está inspirada en el universo 
+  a(href="https://fr.wikipedia.org/wiki/Super_Mario") Super Mario
+  | . Cuando una pieza captura a otra, "rebota" sobre ella hasta que
+  | siguiente casilla en la dirección del viaje. Si está ocupada
+  | entonces la pieza continúa rebotando en la misma dirección, hasta que
+  | encontrarse con una casilla vacía o el borde del tablero. En este ultimo
+  | caso, la pieza de captura se pierde.
+
+p.
+  Las piezas que rebotan son aturdidas por dos jugadas
+  (cuatro medios movimientos): no pueden moverse durante este período.
+  Si son capturados nuevamente durante su aturdimiento, entonces
+  son enviados fuera del tablero de ajedrez, y todas las piezas en camino
+  también desaparecen.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:nrkqnrbb/ppp2ppp/3p4/4p3/1P3P2/8/P1PPP1PP/BNQRNBKR:
+  .diagram.diag22
+    | fen:nukqnrbb/pps2ppp/3s4/4s3/1P6/8/P1PPP1PP/BNQRNBKR:
+  figcaption Antes y después de 1.fxe5
+
+p.
+  Después del movimiento 1.fxe5 en el diagrama, las piezas rojas quedan
+  aturdidas por dos jugadas. Entonces las blancas pueden jugar 2.Bxe5,
+  sacando a relucir el peón que lleva al menos el loco h8 con él.
+  Una posibilidad para las negras serían jugar 1...Qg5, de modo que después
+  de 2.Bxe5 Qxe5 las negras amenacen para sacar el alfil mareado.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:nuk1nrb1/pps2p1p/3s4/4B1q1/1P6/8/P1PPP1PP/1NQRNBKR:
+  .diagram.diag22
+    | fen:nuk1nrb1/pps2p1p/3s4/3qC3/1P6/8/P1PPP1PP/1NQRNBKR:
+  figcaption Después de 1...Qg5 2.Bxe5 Qxe5: el alfil blanco está mareado.
+
+p.
+  El objetivo es capturar al rey contrario. Los movimientos que hacen sacar
+  tu propio rey están prohibidos.
+
+h3 Fuente
+
+p
+  a(href="https://www.jsbeasley.co.uk/encyc.htm")
+    | The Classified Encyclopedia of Chess Variants
+  | , sección 3.6. Esta variante también aparece en chessvariants.com: 
+  a(href="https://www.chessvariants.com/crossover.dir/koopachess.html")
+    | Koopa chess
+  | .
diff --git a/client/src/translations/rules/Koopa/fr.pug b/client/src/translations/rules/Koopa/fr.pug
index e69de29b..b0be96d2 100644
--- a/client/src/translations/rules/Koopa/fr.pug
+++ b/client/src/translations/rules/Koopa/fr.pug
@@ -0,0 +1,54 @@
+p.boxed
+  | Les pièces capturées sont étourdies.
+  | Elles disparaissent si vous les capturez à nouveau.
+
+p
+  | Cette variante est inspirée de l'univers 
+  a(href="https://fr.wikipedia.org/wiki/Super_Mario") Super Mario
+  | . Quand une pièce en capture une autre, elle "rebondit" dessus jusqu'à la
+  | case suivante dans la direction du déplacement. Si celle-ci est occupée,
+  | alors la pièce continue de rebondir dans la même direction, jusqu'à
+  | rencontrer une case vide ou bien le bord de l'échiquier. Dans ce dernier
+  | cas, la pièce capturante est perdue.
+
+p.
+  Les pièces sur lesquelles on rebondit sont étourdies pendant deux coups
+  (quatre demi-coups) : elles ne peuvent pas bouger pendant cette période.
+  Si elles sont capturées à nouveau pendant leur étourdissement, alors elles
+  sont envoyées en dehors de l'échiquier, et toutes les pièces sur leur chemin
+  disparaissent également.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:nrkqnrbb/ppp2ppp/3p4/4p3/1P3P2/8/P1PPP1PP/BNQRNBKR:
+  .diagram.diag22
+    | fen:nukqnrbb/pps2ppp/3s4/4s3/1P6/8/P1PPP1PP/BNQRNBKR:
+  figcaption Avant et après 1.fxe5
+
+p.
+  Après le coup 1.fxe5 sur le diagramme, les pièces rouges sont étourdies
+  pendant deux coups. Ainsi les blancs peuvent jouer 2.Bxe5, faisant sortir
+  le pion qui emmène au moins le fou h8 avec lui. Une possibilité pour les
+  noirs serait de jouer 1...Qg5, pour qu'après 2.Bxe5 Qxe5 les noirs menacent
+  de faire sortir le fou étourdi.
+
+figure.diagram-container
+  .diagram.diag12
+    | fen:nuk1nrb1/pps2p1p/3s4/4B1q1/1P6/8/P1PPP1PP/1NQRNBKR:
+  .diagram.diag22
+    | fen:nuk1nrb1/pps2p1p/3s4/3qC3/1P6/8/P1PPP1PP/1NQRNBKR:
+  figcaption Après 1...Qg5 2.Bxe5 Qxe5 : le fou blanc est étourdi.
+
+p.
+  L'objectif est de capturer le roi adverse. Les coups qui font sortir votre
+  propre roi sont interdits.
+
+h3 Source
+
+p
+  a(href="https://www.jsbeasley.co.uk/encyc.htm")
+    | The Classified Encyclopedia of Chess Variants
+  | , section 3.6. Cette variante est également listée sur chessvariants.com : 
+  a(href="https://www.chessvariants.com/crossover.dir/koopachess.html")
+    | Koopa chess
+  | .
diff --git a/client/src/utils/array.js b/client/src/utils/array.js
index 4d44db02..d02552c1 100644
--- a/client/src/utils/array.js
+++ b/client/src/utils/array.js
@@ -4,7 +4,7 @@ export const ArrayFun = {
     const index = arr.findIndex(rfun);
     if (index >= 0) {
       arr.splice(index, 1);
-      if (all) {
+      if (!!all) {
         // Reverse loop because of the splice below
         for (let i = arr.length - 1; i >= index; i--) {
           if (rfun(arr[i])) arr.splice(i, 1);
diff --git a/client/src/variants/Alice.js b/client/src/variants/Alice.js
index a3d3ff43..82ba6b79 100644
--- a/client/src/variants/Alice.js
+++ b/client/src/variants/Alice.js
@@ -361,10 +361,9 @@ export class AliceRules extends ChessRules {
 
     // Piece or pawn movement
     let notation = piece.toUpperCase() + pawnMark + captureMark + finalSquare;
-    if (["s", "p"].includes(piece) && !["s", "p"].includes(move.appear[0].p)) {
+    if (["s", "p"].includes(piece) && !["s", "p"].includes(move.appear[0].p))
       // Promotion
       notation += "=" + move.appear[0].p.toUpperCase();
-    }
     return notation;
   }
 };
diff --git a/client/src/variants/Apocalypse.js b/client/src/variants/Apocalypse.js
index addf5dda..2439ae5d 100644
--- a/client/src/variants/Apocalypse.js
+++ b/client/src/variants/Apocalypse.js
@@ -150,8 +150,7 @@ export class ApocalypseRules extends ChessRules {
       start: this.whiteMove.start,
       end: this.whiteMove.end,
       appear: this.whiteMove.appear,
-      vanish: this.whiteMove.vanish,
-      illegal: this.whiteMove.illegal
+      vanish: this.whiteMove.vanish
     });
   }
 
@@ -181,7 +180,7 @@ export class ApocalypseRules extends ChessRules {
         const mHash = "m" + vm.start.x + vm.start.y + vm.end.x + vm.end.y;
         if (!moveSet[mHash]) {
           moveSet[mHash] = true;
-          vm.illegal = true; //potentially illegal!
+          vm.end.illegal = true; //potentially illegal!
           speculations.push(vm);
         }
       });
@@ -283,7 +282,7 @@ export class ApocalypseRules extends ChessRules {
             m.vanish[1].c != m.vanish[0].c ||
             // Self-capture attempt
             (
-              !other.illegal &&
+              !other.end.illegal &&
               other.end.x == m.end.x &&
               other.end.y == m.end.y
             )
@@ -292,7 +291,7 @@ export class ApocalypseRules extends ChessRules {
         ||
         (
           m.vanish[0].p == V.PAWN &&
-          !other.illegal &&
+          !other.end.illegal &&
           (
             (
               // Promotion attempt
@@ -319,14 +318,14 @@ export class ApocalypseRules extends ChessRules {
         )
       );
     };
-    if (!!m1.illegal && !isPossible(m1, m2)) {
+    if (!!m1.end.illegal && !isPossible(m1, m2)) {
       // Either an anticipated capture of something which didn't move
       // (or not to the right square), or a push through blocus.
       // ==> Just discard the move, and add a penalty point
       this.penaltyFlags[m1.vanish[0].c]++;
       m1.isNull = true;
     }
-    if (!!m2.illegal && !isPossible(m2, m1)) {
+    if (!!m2.end.illegal && !isPossible(m2, m1)) {
       this.penaltyFlags[m2.vanish[0].c]++;
       m2.isNull = true;
     }
@@ -375,8 +374,8 @@ export class ApocalypseRules extends ChessRules {
       let remain = null;
       const p1 = m1.vanish[0].p;
       const p2 = m2.vanish[0].p;
-      if (!!m1.illegal && !m2.illegal) remain = { c: 'w', p: p1 };
-      else if (!!m2.illegal && !m1.illegal) remain = { c: 'b', p: p2 };
+      if (!!m1.end.illegal && !m2.end.illegal) remain = { c: 'w', p: p1 };
+      else if (!!m2.end.illegal && !m1.end.illegal) remain = { c: 'b', p: p2 };
       if (!remain) {
         // Either both are illegal or both are legal
         if (p1 == V.KNIGHT && p2 == V.PAWN) remain = { c: 'w', p: p1 };
@@ -478,7 +477,7 @@ export class ApocalypseRules extends ChessRules {
     let illegalMoves = [];
     moves.forEach(m => {
       // Warning: m might be illegal!
-      if (!m.illegal) {
+      if (!m.end.illegal) {
         V.PlayOnBoard(this.board, m);
         m.eval = this.evalPosition();
         V.UndoOnBoard(this.board, m);
diff --git a/client/src/variants/Dice.js b/client/src/variants/Dice.js
index 68d80bb4..1b84be42 100644
--- a/client/src/variants/Dice.js
+++ b/client/src/variants/Dice.js
@@ -6,59 +6,139 @@ export class DiceRules extends ChessRules {
     return false;
   }
 
-  doClick(square) {
-    if (
-      this.subTurn == 2 ||
-      isNaN(square[0]) ||
-      this.board[square[0]][square[1]] != V.EMPTY
-    ) {
-      return null;
-    }
-    // Announce the piece' type to be played:
-    return this.getRandPieceMove();
+  static ParseFen(fen) {
+    const fenParts = fen.split(" ");
+    return Object.assign(
+      ChessRules.ParseFen(fen),
+      { toplay: fenParts[5] }
+    );
   }
 
-  getPotentialMovesFrom([x, y]) {
-    if (this.subTurn == 1) return [];
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    this.p2play = [];
+    const toplay = V.ParseFen(fen).toplay;
+    if (toplay != "-") this.p2play.push(toplay);
+  }
+
+  getFen() {
+    return super.getFen() + " " + this.getToplayFen();
+  }
+
+  getFen() {
+    return super.getFenForRepeat() + "_" + this.getToplayFen();
+  }
+
+  getToplayFen() {
     const L = this.p2play.length;
-    const piece = this.getPiece(x, y);
-    if (piece == V.PAWN && this.p2play[L-1] != V.PAWN) {
-      // The piece must be a pawn about to promote.
-      const color = this.turn;
-      const beforeLastRank = (color == 'w' ? 1 : 0);
+    return (L > 0 ? this.p2play[L-1] : "-");
+  }
+
+  static GenRandInitFen(randomness) {
+    return ChessRules.GenRandInitFen(randomness) + " -";
+  }
+
+  canMove(piece, color, [x, y]) {
+    const oppCol = V.GetOppCol(color);
+    if (piece == V.PAWN) {
       const forward = (color == 'w' ? -1 : 1);
-      let moves = [];
-      if (this.board[x + forward][y] == V.EMPTY) {
-        moves.push(
-          this.getBasicMove(
-            [x, y], [x + forward], { c: color, p: this.p2play[L-1] })
-        );
-      }
+      if (this.board[x + forward][y] == V.EMPTY) return true;
       for (let shift of [-1, 1]) {
         const [i, j] = [x + forward, y + shift];
         if (
           V.OnBoard(i, j) &&
           this.board[i][j] != V.EMPTY &&
-          this.getColor(i, j) != color
+          this.getColor(i, j) == oppCol
         ) {
+          return true;
+        }
+      }
+    }
+    else {
+      const steps =
+        [V.KING, V.QUEEN].includes(piece)
+          ? V.steps[V.ROOK].concat(V.steps[V.BISHOP])
+          : V.steps[piece];
+      for (let s of steps) {
+        const [i, j] = [x + s[0], y + s[1]];
+        if (
+          V.OnBoard(i, j) &&
+          (this.board[i][j] == V.EMPTY || this.getColor(i, j) == oppCol)
+        ) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  getRandPiece(color) {
+    // Find pieces which can move and roll a dice
+    let canMove = {};
+    for (let i=0; i<8; i++) {
+      for (let j=0; j<8; j++) {
+        if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
+          const piece = this.getPiece(i, j);
+          if (!canMove[piece] && this.canMove(piece, color, [i, j]))
+            canMove[piece] = [i, j];
+        }
+      }
+    }
+    const options = Object.keys(canMove);
+    const randPiece = options[randInt(options.length)];
+    return [randPiece, canMove[randPiece]];
+  }
+
+  getPotentialMovesFrom([x, y]) {
+    const color = this.turn;
+    let moves = undefined;
+    if (this.movesCount == 0) moves = super.getPotentialMovesFrom([x, y]);
+    else {
+      const L = this.p2play.length; //L is >= 1
+      const piece = this.getPiece(x, y);
+      if (
+        piece == V.PAWN &&
+        this.p2play[L-1] != V.PAWN &&
+        ((color == 'w' && x == 1) || (color == 'b' && x == 6))
+      ) {
+        // The piece is a pawn about to promote
+        const destX = (color == 'w' ? 0 : 7);
+        moves = [];
+        if (this.board[destX][y] == V.EMPTY) {
           moves.push(
             this.getBasicMove(
-              [x, y], [i, j], { c: color, p: this.p2play[L-1] })
+              [x, y], [destX, y], { c: color, p: this.p2play[L-1] })
           );
         }
+        for (let shift of [-1, 1]) {
+          const [i, j] = [destX, y + shift];
+          if (
+            V.OnBoard(i, j) &&
+            this.board[i][j] != V.EMPTY &&
+            this.getColor(i, j) != color
+          ) {
+            moves.push(
+              this.getBasicMove(
+                [x, y], [i, j], { c: color, p: this.p2play[L-1] })
+            );
+          }
+        }
       }
-      return moves;
+      else if (piece != this.p2play[L-1])
+        // The piece type must match last p2play
+        return [];
+      else moves = super.getPotentialMovesFrom([x, y]);
     }
-    if (piece != this.p2play[L-1])
-      // The piece type must match last p2play
-      return [];
-    return super.getPotentialMovesFrom([x, y]);
-  }
-
-  setOtherVariables(fen) {
-    super.setOtherVariables(fen);
-    this.p2play = [];
-    this.subTurn = 1;
+    // Decide which piece the opponent will play:
+    const oppCol = V.GetOppCol(color);
+    moves.forEach(m => {
+      V.PlayOnBoard(this.board, m);
+      const [piece, square] = this.getRandPiece(oppCol);
+      m.start.toplay = square;
+      m.end.piece = piece;
+      V.UndoOnBoard(this.board, m);
+    });
+    return moves;
   }
 
   filterValid(moves) {
@@ -75,91 +155,26 @@ export class DiceRules extends ChessRules {
     return "*";
   }
 
-  play(move) {
-    if (this.subTurn == 1) {
-      this.subTurn = 2;
-      this.p2play.push(move.appear[0].p);
-      return;
-    }
-    // Subturn == 2 means the (dice-constrained) move is played
-    move.flags = JSON.stringify(this.aggregateFlags());
-    V.PlayOnBoard(this.board, move);
-    this.epSquares.push(this.getEpSquare(move));
-    this.movesCount++;
-    this.turn = V.GetOppCol(this.turn);
-    this.subTurn = 1;
-    this.postPlay(move);
-  }
-
   postPlay(move) {
+    this.p2play.push(move.end.piece);
     if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
       this.kingPos[move.vanish[1].c] = [-1, -1];
     // Castle flags for captured king won't be updated (not important...)
     super.postPlay(move);
   }
 
-  undo(move) {
-    if (this.subTurn == 2) {
-      this.subTurn = 1;
-      this.p2play.pop();
-      return;
-    }
-    this.disaggregateFlags(JSON.parse(move.flags));
-    V.UndoOnBoard(this.board, move);
-    this.epSquares.pop();
-    this.movesCount--;
-    this.turn = V.GetOppCol(this.turn);
-    this.subTurn = 2;
-    this.postUndo(move);
-  }
-
   postUndo(move) {
+    this.p2play.pop();
     if (move.vanish.length == 2 && move.vanish[1].p == V.KING)
       this.kingPos[move.vanish[1].c] = [move.vanish[1].x, move.vanish[1].y];
     super.postUndo(move);
   }
 
-  getRandPieceMove() {
-    // For current turn, find pieces which can move and roll a dice
-    let canMove = {};
-    const color = this.turn;
-    for (let i=0; i<8; i++) {
-      for (let j=0; j<8; j++) {
-        if (this.board[i][j] != V.EMPTY && this.getColor(i, j) == color) {
-          const piece = this.getPiece(i, j);
-          if (
-            !canMove[piece] &&
-            super.getPotentialMovesFrom([i, j]).length > 0
-          ) {
-            canMove[piece] = [i, j];
-          }
-        }
-      }
-    }
-    const options = Object.keys(canMove);
-    const randPiece = options[randInt(options.length)];
-    return (
-      new Move({
-        appear: [{ p: randPiece }],
-        vanish: [],
-        start: { x: -1, y: -1 },
-        end: { x: canMove[randPiece][0], y: canMove[randPiece][1] }
-      })
-    );
-  }
-
-  // Random mover
-  getComputerMove() {
-    const toPlay = this.getRandPieceMove();
-    this.play(toPlay);
-    const moves = this.getAllValidMoves();
-    const choice = moves[randInt(moves.length)];
-    this.undo(toPlay);
-    return [toPlay, choice];
+  static get SEARCH_DEPTH() {
+    return 1;
   }
 
   getNotation(move) {
-    if (this.subTurn == 1) return move.appear[0].p.toUpperCase();
-    return super.getNotation(move);
+    return super.getNotation(move) + "/" + move.end.piece.toUpperCase();
   }
 };
diff --git a/client/src/variants/Grand.js b/client/src/variants/Grand.js
index 743f01f9..b88410c9 100644
--- a/client/src/variants/Grand.js
+++ b/client/src/variants/Grand.js
@@ -55,24 +55,24 @@ export class GrandRules extends ChessRules {
 
   setOtherVariables(fen) {
     super.setOtherVariables(fen);
-    const fenParsed = V.ParseFen(fen);
+    const captured = V.ParseFen(fen).captured.split("").map(parseInt);
     // Initialize captured pieces' counts from FEN
     this.captured = {
       w: {
-        [V.ROOK]: parseInt(fenParsed.captured[0]),
-        [V.KNIGHT]: parseInt(fenParsed.captured[1]),
-        [V.BISHOP]: parseInt(fenParsed.captured[2]),
-        [V.QUEEN]: parseInt(fenParsed.captured[3]),
-        [V.MARSHALL]: parseInt(fenParsed.captured[4]),
-        [V.CARDINAL]: parseInt(fenParsed.captured[5])
+        [V.ROOK]: captured[0],
+        [V.KNIGHT]: captured[1],
+        [V.BISHOP]: captured[2],
+        [V.QUEEN]: captured[3],
+        [V.MARSHALL]: captured[4],
+        [V.CARDINAL]: captured[5]
       },
       b: {
-        [V.ROOK]: parseInt(fenParsed.captured[6]),
-        [V.KNIGHT]: parseInt(fenParsed.captured[7]),
-        [V.BISHOP]: parseInt(fenParsed.captured[8]),
-        [V.QUEEN]: parseInt(fenParsed.captured[9]),
-        [V.MARSHALL]: parseInt(fenParsed.captured[10]),
-        [V.CARDINAL]: parseInt(fenParsed.captured[11])
+        [V.ROOK]: captured[6],
+        [V.KNIGHT]: captured[7],
+        [V.BISHOP]: captured[8],
+        [V.QUEEN]: captured[9],
+        [V.MARSHALL]: captured[10],
+        [V.CARDINAL]: captured[11]
       }
     };
   }
diff --git a/client/src/variants/Koopa.js b/client/src/variants/Koopa.js
index 9ca83848..f24e5b2f 100644
--- a/client/src/variants/Koopa.js
+++ b/client/src/variants/Koopa.js
@@ -1,22 +1,113 @@
-import { ChessRulesi, PiPo } from "@/base_rules";
+import { ChessRules, PiPo } from "@/base_rules";
 
 export class KoopaRules extends ChessRules {
   static get HasEnpassant() {
     return false;
   }
 
-  // Between stun time and stun + 1 move
-  static get STUNNED_1() {
+  static get STUNNED() {
     return ['s', 'u', 'o', 'c', 't', 'l'];
   }
 
-  // Between stun + 1 move and stun + 2 moves
-  static get STUNNED_2() {
-    return ['v', 'x', 'a', 'd', 'w', 'm'];
+  static get PIECES() {
+    return ChessRules.PIECES.concat(V.STUNNED);
   }
 
-  static get PIECES() {
-    return ChessRules.PIECES.concat(V.STUNNED_1).concat(V.STUNNED_2);
+  static ParseFen(fen) {
+    let res = ChessRules.ParseFen(fen);
+    const fenParts = fen.split(" ");
+    res.stunned = fenParts[4];
+    return res;
+  }
+
+  static IsGoodFen(fen) {
+    if (!ChessRules.IsGoodFen(fen)) return false;
+    const fenParsed = V.ParseFen(fen);
+    // 5) Check "stunned"
+    if (
+      !fenParsed.stunned ||
+      (
+        fenParsed.stunned != "-" &&
+        !fenParsed.stunned.match(/^([a-h][1-8][1-4],?)*$/)
+      )
+    ) {
+      return false;
+    }
+    return true;
+  }
+
+  getPpath(b) {
+    return (V.STUNNED.includes(b[1]) ? "Koopa/" : "") + b;
+  }
+
+  getFen() {
+    return super.getFen() + " " + this.getStunnedFen();
+  }
+
+  getFenForRepeat() {
+    return super.getFenForRepeat() + "_" + this.getStunnedFen();
+  }
+
+  getStunnedFen() {
+    return (
+      Object.keys(this.stunned)
+      .map(square => square + this.stunned[square])
+      .join(",")
+    );
+  }
+
+  // Base GenRandInitFen() is fine because en-passant indicator will
+  // stand for stunned indicator.
+
+  scanKings(fen) {
+    this.INIT_COL_KING = { w: -1, b: -1 };
+    // Squares of white and black king:
+    this.kingPos = { w: [-1, -1], b: [-1, -1] };
+    const fenRows = V.ParseFen(fen).position.split("/");
+    const startRow = { 'w': V.size.x - 1, 'b': 0 };
+    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];
+            this.INIT_COL_KING["b"] = k;
+            break;
+          case "K":
+          case "L":
+            this.kingPos["w"] = [i, k];
+            this.INIT_COL_KING["w"] = k;
+            break;
+          default: {
+            const num = parseInt(fenRows[i].charAt(j));
+            if (!isNaN(num)) k += num - 1;
+          }
+        }
+        k++;
+      }
+    }
+  }
+
+  setOtherVariables(fen) {
+    super.setOtherVariables(fen);
+    let stunnedArray = [];
+    const stunnedFen = V.ParseFen(fen).stunned;
+    if (stunnedFen != "-") {
+      stunnedArray =
+        stunnedFen
+        .split(",")
+        .map(s => {
+          return {
+            square: s.substr(0, 2),
+            state: parseInt(s[2])
+          };
+        });
+    }
+    this.stunned = {};
+    stunnedArray.forEach(s => {
+      this.stunned[s.square] = s.state;
+    });
   }
 
   getNormalizedStep(step) {
@@ -31,13 +122,14 @@ export class KoopaRules extends ChessRules {
   getPotentialMovesFrom([x, y]) {
     let moves = super.getPotentialMovesFrom([x, y]);
     // Complete moves: stuns & kicks
-    const stun = V.STUNNED_1.concat(V.STUNNED_2);
+    let promoteAfterStun = [];
+    const color = this.turn;
     moves.forEach(m => {
       if (m.vanish.length == 2 && m.appear.length == 1) {
         const step =
           this.getNormalizedStep([m.end.x - m.start.x, m.end.y - m.start.y]);
         // "Capture" something: is target stunned?
-        if (stun.includes(m.vanish[1].p)) {
+        if (V.STUNNED.includes(m.vanish[1].p)) {
           // Kick it: continue movement in the same direction,
           // destroying all on its path.
           let [i, j] = [m.end.x + step[0], m.end.y + step[1]];
@@ -58,32 +150,35 @@ export class KoopaRules extends ChessRules {
         }
         else {
           // The piece is now stunned
-          m.appear.push(m.vanish.pop());
+          m.appear.push(JSON.parse(JSON.stringify(m.vanish[1])));
           const pIdx = ChessRules.PIECES.findIndex(p => p == m.appear[1].p);
-          m.appear[1].p = V.STUNNED_1[pIdx];
+          m.appear[1].p = V.STUNNED[pIdx];
           // And the capturer continue in the same direction until an empty
           // square or the edge of the board, maybe stunning other pieces.
           let [i, j] = [m.end.x + step[0], m.end.y + step[1]];
           while (V.OnBoard(i, j) && this.board[i][j] != V.EMPTY) {
             const colIJ = this.getColor(i, j);
             const pieceIJ = this.getPiece(i, j);
-            m.vanish.push(
-              new PiPo({
-                x: i,
-                y: j,
-                c: colIJ,
-                p: pieceIJ
-              })
-            );
-            const pIdx = ChessRules.PIECES.findIndex(p => p == pieceIJ);
-            m.appear.push(
-              new PiPo({
-                x: i,
-                y: j,
-                c: colIJ,
-                p: V.STUNNED_1[pIdx]
-              })
-            );
+            let pIdx = ChessRules.PIECES.findIndex(p => p == pieceIJ);
+            if (pIdx >= 0) {
+              // The piece isn't already stunned
+              m.vanish.push(
+                new PiPo({
+                  x: i,
+                  y: j,
+                  c: colIJ,
+                  p: pieceIJ
+                })
+              );
+              m.appear.push(
+                new PiPo({
+                  x: i,
+                  y: j,
+                  c: colIJ,
+                  p: V.STUNNED[pIdx]
+                })
+              );
+            }
             i += step[0];
             j += step[1];
           }
@@ -91,27 +186,36 @@ export class KoopaRules extends ChessRules {
             m.appear[0].x = i;
             m.appear[0].y = j;
             // Is it a pawn on last rank?
+            if ((color == 'w' && i == 0) || (color == 'b' && i == 7)) {
+              m.appear[0].p = V.ROOK;
+              for (let ppiece of [V.KNIGHT, V.BISHOP, V.QUEEN]) {
+                let mp = JSON.parse(JSON.stringify(m));
+                mp.appear[0].p = ppiece;
+                promoteAfterStun.push(mp);
+              }
+            }
           }
-          else {
+          else
             // The piece is out
             m.appear.shift();
-          }
         }
       }
     });
-    return moves;
-  }
-
-  static GenRandInitFen(randomness) {
-    // No en-passant:
-    return ChessRules.GenRandInitFen(randomness).slice(0, -2);
+    return moves.concat(promoteAfterStun);
   }
 
   filterValid(moves) {
     // Forbid kicking own king out
     const color = this.turn;
     return moves.filter(m => {
-      return m.vanish.every(v => v.c != color || !(['l','m'].includes(v.p)));
+      const kingAppear = m.appear.some(a => a.c == color && a.p == V.KING);
+      return m.vanish.every(v => {
+        return (
+          v.c != color ||
+          !["k", "l"].includes(v.p) ||
+          (v.p == "k" && kingAppear)
+        );
+      });
     });
   }
 
@@ -127,13 +231,114 @@ export class KoopaRules extends ChessRules {
   }
 
   postPlay(move) {
-    // TODO: toutes les pièces "stunned" by me (turn) avancent d'un niveau
-    // --> alter board
-    move.wasStunned = array of stunned stage 2 pieces (just back to normal then)
+    // Base method is fine because a stunned king (which won't be detected)
+    // can still castle after going back to normal.
+    super.postPlay(move);
+    const kIdx = move.vanish.findIndex(v => v.p == "l");
+    if (kIdx >= 0)
+      // A stunned king vanish (game over)
+      this.kingPos[move.vanish[kIdx].c] = [-1, -1];
+    move.stunned = JSON.stringify(this.stunned);
+    // Array of stunned stage 1 pieces (just back to normal then)
+    Object.keys(this.stunned).forEach(square => {
+      // All (formerly) stunned pieces progress by 1 level, if still on board
+      const coords = V.SquareToCoords(square);
+      const [x, y] = [coords.x, coords.y];
+      if (V.STUNNED.includes(this.board[x][y][1])) {
+        // Stunned piece still on board
+        this.stunned[square]--;
+        if (this.stunned[square] == 0) {
+          delete this.stunned[square];
+          const color = this.getColor(x, y);
+          const piece = this.getPiece(x, y);
+          const pIdx = V.STUNNED.findIndex(p => p == piece);
+          this.board[x][y] = color + ChessRules.PIECES[pIdx];
+        }
+      }
+      else delete this.stunned[square];
+    });
+    // Any new stunned pieces?
+    move.appear.forEach(a => {
+      if (V.STUNNED.includes(a.p))
+        // Set to maximum stun level:
+        this.stunned[V.CoordsToSquare({ x: a.x, y: a.y })] = 4;
+    });
   }
 
   postUndo(move) {
-    if (wasStunned
-      STUNNED_2
+    super.postUndo(move);
+    const kIdx = move.vanish.findIndex(v => v.p == "l");
+    if (kIdx >= 0) {
+      // A stunned king vanished
+      this.kingPos[move.vanish[kIdx].c] =
+        [move.vanish[kIdx].x, move.vanish[kIdx].y];
+    }
+    this.stunned = JSON.parse(move.stunned);
+    for (let i=0; i<8; i++) {
+      for (let j=0; j<8; j++) {
+        const square = V.CoordsToSquare({ x: i, y: j });
+        const pieceIJ = this.getPiece(i, j);
+        if (!this.stunned[square]) {
+          const pIdx = V.STUNNED.findIndex(p => p == pieceIJ);
+          if (pIdx >= 0)
+            this.board[i][j] = this.getColor(i, j) + ChessRules.PIECES[pIdx];
+        }
+        else {
+          const pIdx = ChessRules.PIECES.findIndex(p => p == pieceIJ);
+          if (pIdx >= 0)
+            this.board[i][j] = this.getColor(i, j) + V.STUNNED[pIdx];
+        }
+      }
+    }
+  }
+
+  static get VALUES() {
+    return Object.assign(
+      {
+        s: 1,
+        u: 5,
+        o: 3,
+        c: 3,
+        t: 9,
+        l: 1000
+      },
+      ChessRules.VALUES
+    );
+  }
+
+  static get SEARCH_DEPTH() {
+    return 2;
+  }
+
+  getNotation(move) {
+    if (
+      move.appear.length == 2 &&
+      move.vanish.length == 2 &&
+      move.appear.concat(move.vanish).every(
+        av => ChessRules.PIECES.includes(av.p)) &&
+      move.appear[0].p == V.KING
+    ) {
+      if (move.end.y < move.start.y) return "0-0-0";
+      return "0-0";
+    }
+    const finalSquare = V.CoordsToSquare(move.end);
+    const piece = this.getPiece(move.start.x, move.start.y);
+    const captureMark = move.vanish.length >= 2 ? "x" : "";
+    let pawnMark = "";
+    if (piece == 'p' && captureMark.length == 1)
+      pawnMark = V.CoordToColumn(move.start.y); //start column
+    // Piece or pawn movement
+    let notation =
+      (piece == V.PAWN ? pawnMark : piece.toUpperCase()) +
+      captureMark + finalSquare;
+    if (
+      piece == 'p' &&
+      move.appear[0].c == move.vanish[0].c &&
+      move.appear[0].p != 'p'
+    ) {
+      // Promotion
+      notation += "=" + move.appear[0].p.toUpperCase();
+    }
+    return notation;
   }
 };
diff --git a/client/src/views/Faq.vue b/client/src/views/Faq.vue
index 3376afd2..71647173 100644
--- a/client/src/views/Faq.vue
+++ b/client/src/views/Faq.vue
@@ -50,6 +50,7 @@ export default {
 
 <style lang="sass">
 #faqDiv
+  margin-bottom: 10px
   @media screen and (max-width: 767px)
     margin-left: var(--universal-margin)
     margin-right: var(--universal-margin)
diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue
index 13632041..6835b556 100644
--- a/client/src/views/MyGames.vue
+++ b/client/src/views/MyGames.vue
@@ -6,7 +6,7 @@ main
         button.tabbtn#liveGames(@click="setDisplay('live',$event)")
           | {{ st.tr["Live games"] }}
         button.tabbtn#corrGames(@click="setDisplay('corr',$event)")
-          | {{ st.tr["Correspondance games"] }}
+          | {{ st.tr["Correspondence games"] }}
         button.tabbtn#importGames(@click="setDisplay('import',$event)")
           | {{ st.tr["Imported games"] }}
       GameList(
diff --git a/server/models/User.js b/server/models/User.js
index 37df4e6c..c50cdb79 100644
--- a/server/models/User.js
+++ b/server/models/User.js
@@ -107,7 +107,7 @@ const UserModel = {
         query =
           "UPDATE Users " +
           // Also empty the login token to invalidate future attempts
-          "SET loginToken = NULL" +
+          "SET loginToken = NULL, loginTime = NULL " +
           setSessionToken + " " +
           "WHERE id = " + id;
         db.run(query);
-- 
2.44.0