From: Benjamin Auder <benjamin.auder@somewhere>
Date: Fri, 20 Mar 2020 20:05:12 +0000 (+0100)
Subject: Add Coregal variant + simplify time management in Game.vue
X-Git-Url: https://git.auder.net/variants/Apocalypse/scripts/current/pieces/doc/DESCRIPTION?a=commitdiff_plain;h=3f22c2c3939dfd6bd66da26e6d6d9848c6da86d2;p=vchess.git

Add Coregal variant + simplify time management in Game.vue
---

diff --git a/client/src/base_rules.js b/client/src/base_rules.js
index 71fa13cf..b83a409b 100644
--- a/client/src/base_rules.js
+++ b/client/src/base_rules.js
@@ -829,7 +829,7 @@ export const ChessRules = class ChessRules {
       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
+      // If this code is reached, rook and king are on initial position
 
       // NOTE: in some variants this is not a rook, but let's keep variable name
       const rookPos = this.castleFlags[c][castleSide];
diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue
index 65e96cf6..e86c551e 100644
--- a/client/src/components/BaseGame.vue
+++ b/client/src/components/BaseGame.vue
@@ -203,10 +203,12 @@ export default {
       this.positionCursorTo(this.moves.length - 1);
       this.incheck = this.vr.getCheckSquares(this.vr.turn);
       const score = this.vr.getCurrentScore();
-      if (["1-0","0-1"].includes(score))
-        this.moves[L - 1].notation += "#";
-      else if (this.vr.getCheckSquares(this.vr.turn).length > 0)
-        this.moves[L - 1].notation += "+";
+      if (L > 0 && this.moves[L - 1].notation != "...") {
+        if (["1-0","0-1"].includes(score))
+          this.moves[L - 1].notation += "#";
+        else if (this.vr.getCheckSquares(this.vr.turn).length > 0)
+          this.moves[L - 1].notation += "+";
+      }
     },
     positionCursorTo: function(index) {
       this.cursor = index;
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index 5d093742..ded2b8a9 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -197,5 +197,6 @@ export const translations = {
   "Squares disappear": "Squares disappear",
   "Standard rules": "Standard rules",
   "Transform an essay": "Transform an essay",
+  "Two royal pieces": "Two royal pieces",
   "Unidentified pieces": "Unidentified pieces"
 };
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index 877f8587..c065cf0e 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -197,5 +197,6 @@ export const translations = {
   "Squares disappear": "Las casillas desaparecen",
   "Standard rules": "Reglas estandar",
   "Transform an essay": "Transformar un ensayo",
+  "Two royal pieces": "Dos piezas reales",
   "Unidentified pieces": "Piezas no identificadas"
 };
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index cf32f6e4..120571b7 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -197,5 +197,6 @@ export const translations = {
   "Squares disappear": "Les cases disparaissent",
   "Standard rules": "Règles usuelles",
   "Transform an essay": "Transformer un essai",
+  "Two royal pieces": "Deux pièces royales",
   "Unidentified pieces": "Pièces non identifiées"
 };
diff --git a/client/src/translations/rules/Coregal/en.pug b/client/src/translations/rules/Coregal/en.pug
index 3a33838b..e9eb1ed8 100644
--- a/client/src/translations/rules/Coregal/en.pug
+++ b/client/src/translations/rules/Coregal/en.pug
@@ -1,2 +1,51 @@
 p.boxed
-  | TODO
+  | Checkmating the queen wins too. A queen cannot go or stay under check.
+
+p Just as the king, the queen can be checked and mated. This means that
+ul
+  li It is not allowed to make a move such that the queen can be captured.
+  li.
+    When your queen is attacked, you must play a move such that the queen
+    is no longer attacked.
+  li If it's impossible, then you lose.
+
+p.
+  Since the king remains royal, this allows a new way to win: check both
+  royal pieces at the same time, like on the following diagram.
+
+figure.diagram-container
+  .diagram
+    | fen:4Q3/4K3/8/8/3N4/5k2/2q5/8:
+  figcaption Both black king and queen are in check: white wins.
+
+h3 Special moves
+
+p.
+  If a pawn promotes into a queen, the latter is royal as well.
+  So under-promotions might be wiser.
+
+p.
+  You can castle with the queen or the king and any of the two rooks,
+  under the same conditions as orthodox castling.
+  Here is the resulting position after two white small castles and
+  one black large castle with the queen:
+
+figure.diagram-container
+  .diagram
+    | fen:r4rq1/ppppppkp/6p1/8/8/8/PPPPPPPP/1QR2RK1:
+  figcaption After two white small castles and one black large castle.
+
+p.
+  Note: to castle in a game you need to select
+  the king or queen first, and then move it to a rook.
+
+h3 Source
+
+p
+  a(href="https://www.chessvariants.com/winning.dir/coregal.html") Coregal Chess
+  | &nbsp;on chessvariants.com.
+  | This variant can be played too 
+  a(href="https://greenchess.net/rules.php?v=coregal") on greenchess.net
+  | .
+
+p Inventor: Vernon R. Parton (1970)
diff --git a/client/src/translations/rules/Coregal/es.pug b/client/src/translations/rules/Coregal/es.pug
index 3a33838b..489775b9 100644
--- a/client/src/translations/rules/Coregal/es.pug
+++ b/client/src/translations/rules/Coregal/es.pug
@@ -1,2 +1,53 @@
 p.boxed
-  | TODO
+  | Es posible ganar dando jaque mate a la dama.
+  | Una dama no puede ir o permanecer en jaque.
+
+p Al igual que el rey, la dama puede ser en jaque (mate). Es decir que
+ul
+  li Se prohíbe un movimiento que permita al oponente capturar a la dama.
+  li.
+    Cuando tu dama es atacada, debes hacer un movimiento
+    para que ya no sea atacado.
+  li Si es imposible, entonces has perdido.
+
+p.
+  Como el rey sigue siendo real, esto agrega una nueva forma de ganar:
+  jaque las dos piezas reales al mismo tiempo, como en el siguiente diagrama.
+
+figure.diagram-container
+  .diagrama
+    | fen:4Q3/4K3/8/8/3N4/5k2/2q5/8:
+  figcaption Las blancas ganan porque el rey y la dama negra están en jaque.
+
+h3 Movimientos especiales
+
+p.
+  Si un peón es promovido a reina, este último también es real.
+  Entonces, las sub-promociones pueden ser más sabias.
+
+p.
+  Puedes hacer el enroque con la dama o el rey y cualquiera de los
+  dos torres, en las mismas condiciones que para el ajedrez ortodoxo.
+  Aquí hay una posible posición después de dos pequeñas rocas blancas y
+  un gran enroque negro con la dama:
+
+figure.diagram-container
+  .diagrama
+    | fen:r4rq1/ppppppkp/6p1/8/8/8/PPPPPPPP/1QR2RK1:
+  figcaption Después de dos pequeñas rocas blancas y un gran enroque negro.
+
+p.
+  Nota: para enrocarse en una partida debes seleccionar el rey o la dama
+  primero, luego mueva la pieza a una torre.
+
+h3 Fuente
+
+p
+  | La 
+  a(href="https://www.chessvariants.com/winning.dir/coregal.html") cariante Coregal
+  | &nbsp;en chessvariants.com.
+  | Esta variante también es jugable
+  a(href="https://greenchess.net/rules.php?v=coregal") en greenchess.net
+  | .
+
+p Inventor: Vernon R. Parton (1970)
diff --git a/client/src/translations/rules/Coregal/fr.pug b/client/src/translations/rules/Coregal/fr.pug
index 3a33838b..47247f15 100644
--- a/client/src/translations/rules/Coregal/fr.pug
+++ b/client/src/translations/rules/Coregal/fr.pug
@@ -1,2 +1,53 @@
 p.boxed
-  | TODO
+  | On peut gagner en matant la dame. Une dame ne peut aller ou rester en échec.
+
+p Tout comme le roi, la dame peut être mise en échec et matée. C'est-à-dire que
+ul
+  li Un coup qui laisserait l'adversaire capturer la dame est interdit.
+  li.
+    Quand votre dame est attaquée, vous devez jouer un coup faisant
+    en sorte que celle-ci ne soit plus attaquée.
+  li Si c'est impossible, alors vous avez perdu.
+
+p.
+  Puisque le roi reste royal, ceci ajoute une nouvelle manière de gagner :
+  mettre en échc les deux pièces royales en même temps,
+  comme sur le diagramme suivant.
+
+figure.diagram-container
+  .diagram
+    | fen:4Q3/4K3/8/8/3N4/5k2/2q5/8:
+  figcaption Les blancs gagnent car le roi et la dame noir sont en échec.
+
+h3 Coups spéciaux
+
+p.
+  Si un pion est promu en dame, cette dernière est royale également.
+  Ainsi, les sous promotions peuvent être plus sages.
+
+p.
+  Vous pouvez roquer avec la dame ou le roi et n'importe laquelle des
+  deux tours, sous les mêmes conditions qu'aux échecs orthodoxes.
+  Voici une position possible après deux petits roques blancs et
+  un grand roque noir avec la dame :
+
+figure.diagram-container
+  .diagram
+    | fen:r4rq1/ppppppkp/6p1/8/8/8/PPPPPPPP/1QR2RK1:
+  figcaption Après deux petits roques blancs et un grand roque noir.
+
+p.
+  Note : pour roquer dans une partie il faut sélectionner le roi ou la dame
+  d'abord, puis déplacer la pièce sur une tour.
+
+h3 Source
+
+p
+  | La 
+  a(href="https://www.chessvariants.com/winning.dir/coregal.html") variante Coregal
+  | &nbsp;sur chessvariants.com.
+  | Cette variante est jouable également 
+  a(href="https://greenchess.net/rules.php?v=coregal") sur greenchess.net
+  | .
+
+p Inventeur : Vernon R. Parton (1970)
diff --git a/client/src/variants/Coregal.js b/client/src/variants/Coregal.js
index ff4f1dfa..1a4a4038 100644
--- a/client/src/variants/Coregal.js
+++ b/client/src/variants/Coregal.js
@@ -1,10 +1,11 @@
-import { ChessRules } from "@/base_rules";
+import { ChessRules, Move, PiPo } from "@/base_rules";
 import { ArrayFun } from "@/utils/array";
 import { randInt, sample } from "@/utils/alea";
 
 export class CoregalRules extends ChessRules {
   static IsGoodPosition(position) {
     if (!super.IsGoodPosition(position)) return false;
+    const rows = position.split("/");
     // Check that at least one queen of each color is there:
     let queens = {};
     for (let row of rows) {
@@ -19,11 +20,37 @@ export class CoregalRules extends ChessRules {
     return !!flags.match(/^[a-z]{8,8}$/);
   }
 
+  // Scanning king position for faster updates is still interesting,
+  // but no need for INIT_COL_KING because it's given in castle flags.
+  scanKings(fen) {
+    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;
+      for (let j = 0; j < fenRows[i].length; j++) {
+        switch (fenRows[i].charAt(j)) {
+          case "k":
+            this.kingPos["b"] = [i, k];
+            break;
+          case "K":
+            this.kingPos["w"] = [i, k];
+            break;
+          default: {
+            const num = parseInt(fenRows[i].charAt(j));
+            if (!isNaN(num)) k += num - 1;
+          }
+        }
+        k++;
+      }
+    }
+  }
+
   getCheckSquares(color) {
     let squares = [];
     const oppCol = V.GetOppCol(color);
     if (this.isAttacked(this.kingPos[color], oppCol))
-      squares.push(this.kingPos[color]);
+      squares.push(JSON.parse(JSON.stringify(this.kingPos[color])));
     for (let i=0; i<V.size.x; i++) {
       for (let j=0; j<V.size.y; j++) {
         if (
@@ -113,8 +140,8 @@ export class CoregalRules extends ChessRules {
       pieces[c][bishop2Pos] = "b";
       pieces[c][knight2Pos] = "n";
       pieces[c][rook2Pos] = "r";
-      flags += V.CoordToColumn(rook1Pos) + V.CoordToColumn(queenPos) +
-               V.CoordToColumn(kingPos) + V.CoordToColumn(rook2Pos);
+      flags +=
+        [rook1Pos, queenPos, kingPos, rook2Pos].sort().map(V.CoordToColumn).join("");
     }
     // Add turn + flags + enpassant
     return (
@@ -126,11 +153,11 @@ export class CoregalRules extends ChessRules {
   }
 
   setFlags(fenflags) {
-    // white a-castle, h-castle, black a-castle, h-castle
+    // white pieces positions, then black pieces positions
     this.castleFlags = { w: [...Array(4)], b: [...Array(4)] };
     for (let i = 0; i < 8; i++) {
       this.castleFlags[i < 4 ? "w" : "b"][i % 4] =
-        V.ColumnToCoord(fenflags.charAt(i));
+        V.ColumnToCoord(fenflags.charAt(i))
     }
   }
 
@@ -138,90 +165,90 @@ export class CoregalRules extends ChessRules {
     return super.getPotentialQueenMoves(sq).concat(this.getCastleMoves(sq));
   }
 
-  getCastleMoves([x, y], castleInCheck) {
-    return [];
-//    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
-//
-//      // NOTE: in some variants this is not a rook, but let's keep variable name
-//      const rookPos = this.castleFlags[c][castleSide];
-//      const castlingPiece = this.getPiece(x, rookPos);
-//      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));
-//      i = y;
-//      do {
-//        if (
-//          (!castleInCheck && 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, castlingPiece].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;
-//      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: castlingPiece, c: c })
-//          ],
-//          vanish: [
-//            new PiPo({ x: x, y: y, p: V.KING, c: c }),
-//            new PiPo({ x: x, y: rookPos, p: castlingPiece, c: c })
-//          ],
-//          end:
-//            Math.abs(y - rookPos) <= 2
-//              ? { x: x, y: rookPos }
-//              : { x: x, y: y + 2 * (castleSide == 0 ? -1 : 1) }
-//        })
-//      );
-//    }
-//
-//    return moves;
+  getCastleMoves([x, y]) {
+    const c = this.getColor(x, y);
+    if (
+      x != (c == "w" ? V.size.x - 1 : 0) ||
+      !this.castleFlags[c].slice(1, 3).includes(y)
+    ) {
+      // x isn't first rank, or piece moved
+      return [];
+    }
+    const castlingPiece = this.getPiece(x, y);
+
+    // Relative position of the selected piece: left or right ?
+    // If left: small castle left, large castle right.
+    // If right: usual situation.
+    const relPos = (this.castleFlags[c][1] == y ? "left" : "right");
+
+    // Castling ?
+    const oppCol = V.GetOppCol(c);
+    let moves = [];
+    let i = 0;
+    // Castling piece, then rook:
+    const finalSquares = {
+      0: (relPos == "left" ? [1, 2] : [2, 3]),
+      3: (relPos == "right" ? [6, 5] : [5, 4])
+    };
+
+    // Left, then right castle:
+    castlingCheck: for (let castleSide of [0, 3]) {
+      if (this.castleFlags[c][castleSide] >= 8) continue;
+
+      // Rook and castling piece are on initial position
+      const rookPos = this.castleFlags[c][castleSide];
+
+      // 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 ||
+              ![castlingPiece, 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;
+      for (i = y + step; i != rookPos; i += step) {
+        if (this.board[x][i] != V.EMPTY) continue castlingCheck;
+      }
+
+      // Nothing on final squares, except maybe castling piece and rook?
+      for (i = 0; i < 2; i++) {
+        if (
+          this.board[x][finalSquares[castleSide][i]] != V.EMPTY &&
+          ![y, rookPos].includes(finalSquares[castleSide][i])
+        ) {
+          continue castlingCheck;
+        }
+      }
+
+      // If this code is reached, castle is valid
+      moves.push(
+        new Move({
+          appear: [
+            new PiPo({ x: x, y: finalSquares[castleSide][0], p: castlingPiece, c: c }),
+            new PiPo({ x: x, y: finalSquares[castleSide][1], p: V.ROOK, c: c })
+          ],
+          vanish: [
+            new PiPo({ x: x, y: y, p: castlingPiece, c: c }),
+            new PiPo({ x: x, y: rookPos, p: V.ROOK, c: c })
+          ],
+          // In this variant, always castle by playing onto the rook
+          end: { x: x, y: rookPos }
+        })
+      );
+    }
+
+    return moves;
   }
 
   underCheck(color) {
@@ -242,27 +269,55 @@ export class CoregalRules extends ChessRules {
   }
 
   updateCastleFlags(move, piece) {
-//    const c = V.GetOppCol(this.turn);
-//    const firstRank = (c == "w" ? V.size.x - 1 : 0);
-//    // Update castling flags if rooks are moved
-//    const oppCol = V.GetOppCol(c);
-//    const oppFirstRank = V.size.x - 1 - firstRank;
-//    if (piece == V.KING && move.appear.length > 0)
-//      this.castleFlags[c] = [V.size.y, V.size.y];
-//    else if (
-//      move.start.x == firstRank && //our rook moves?
-//      this.castleFlags[c].includes(move.start.y)
-//    ) {
-//      const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 1);
-//      this.castleFlags[c][flagIdx] = V.size.y;
-//    } else if (
-//      move.end.x == oppFirstRank && //we took opponent rook?
-//      this.castleFlags[oppCol].includes(move.end.y)
-//    ) {
-//      const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 1);
-//      this.castleFlags[oppCol][flagIdx] = V.size.y;
-//    }
+    const c = V.GetOppCol(this.turn);
+    const firstRank = (c == "w" ? V.size.x - 1 : 0);
+    // Update castling flags if castling pieces moved or were captured
+    const oppCol = V.GetOppCol(c);
+    const oppFirstRank = V.size.x - 1 - firstRank;
+    if (move.start.x == firstRank && [V.KING, V.QUEEN].includes(piece)) {
+      if (this.castleFlags[c][1] == move.start.y)
+        this.castleFlags[c][1] = 8;
+      else if (this.castleFlags[c][2] == move.start.y)
+        this.castleFlags[c][2] = 8;
+      // Else: the flag is already turned off
+    }
+    else if (
+      move.start.x == firstRank && //our rook moves?
+      [this.castleFlags[c][0], this.castleFlags[c][3]].includes(move.start.y)
+    ) {
+      const flagIdx = (move.start.y == this.castleFlags[c][0] ? 0 : 3);
+      this.castleFlags[c][flagIdx] = 8;
+    } else if (
+      move.end.x == oppFirstRank && //we took opponent rook?
+      [this.castleFlags[oppCol][0], this.castleFlags[oppCol][3]].includes(move.end.y)
+    ) {
+      const flagIdx = (move.end.y == this.castleFlags[oppCol][0] ? 0 : 3);
+      this.castleFlags[oppCol][flagIdx] = 8;
+    }
   }
 
   // NOTE: do not set queen value to 1000 or so, because there may be several.
+
+  getNotation(move) {
+    if (move.appear.length == 2) {
+      // Castle: determine the right notation
+      const color = move.appear[0].c;
+      let symbol = (move.appear[0].p == V.QUEEN ? "Q" : "") + "0-0";
+      if (
+        (
+          this.castleFlags[color][1] == move.vanish[0].y &&
+          move.end.y > move.start.y
+        )
+        ||
+        (
+          this.castleFlags[color][2] == move.vanish[0].y &&
+          move.end.y < move.start.y
+        )
+      ) {
+        symbol += "-0";
+      }
+      return symbol;
+    }
+    return super.getNotation(move);
+  }
 };
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index ac6ed4d5..6339a3ce 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -288,11 +288,11 @@ export default {
         // Discard potential "/?next=[...]" for page indication:
         encodeURIComponent(this.$route.path.match(/\/game\/[a-zA-Z0-9]+/)[0]);
       this.conn = new WebSocket(this.connexionString);
-      this.conn.onmessage = this.socketMessageListener;
-      this.conn.onclose = this.socketCloseListener;
+      this.conn.addEventListener("message", this.socketMessageListener);
+      this.conn.addEventListener("close", this.socketCloseListener);
       // Socket init required before loading remote game:
       const socketInit = callback => {
-        if (!!this.conn && this.conn.readyState == 1)
+        if (this.conn.readyState == 1)
           // 1 == OPEN state
           callback();
         else
@@ -480,6 +480,8 @@ export default {
         }
         case "killed":
           // I logged in elsewhere:
+          this.conn.removeEventListener("message", this.socketMessageListener);
+          this.conn.removeEventListener("close", this.socketCloseListener);
           this.conn = null;
           alert(this.st.tr["New connexion detected: tab now offline"]);
           break;
@@ -567,7 +569,7 @@ export default {
             .filter(k =>
               [
                 "id","fen","players","vid","cadence","fenStart","vname",
-                "moves","clocks","initime","score","drawOffer","rematchOffer"
+                "moves","clocks","score","drawOffer","rematchOffer"
               ].includes(k))
             .reduce(
               (obj, k) => {
@@ -591,13 +593,11 @@ export default {
         case "lastate": {
           // Got opponent infos about last move
           this.gotLastate = true;
-          if (!data.data.nothing) {
-            this.lastate = data.data;
-            if (this.game.rendered)
-              // Game is rendered (Board component)
-              this.processLastate();
-            // Else: will be processed when game is ready
-          }
+          this.lastate = data.data;
+          if (this.game.rendered)
+            // Game is rendered (Board component)
+            this.processLastate();
+          // Else: will be processed when game is ready
           break;
         }
         case "newmove": {
@@ -636,12 +636,11 @@ export default {
                 }
               }
               this.$refs["basegame"].play(movePlus.move, "received", null, true);
+              const moveColIdx = ["w", "b"].indexOf(movePlus.color);
+              this.game.clocks[moveColIdx] = movePlus.clock;
               this.processMove(
                 movePlus.move,
-                {
-                  clock: movePlus.clock,
-                  receiveMyMove: receiveMyMove
-                }
+                { receiveMyMove: receiveMyMove }
               );
             }
           }
@@ -649,9 +648,8 @@ export default {
         }
         case "gotmove": {
           this.opponentGotMove = true;
-          // Now his clock starts running:
+          // Now his clock starts running on my side:
           const oppIdx = ['w','b'].indexOf(this.vr.turn);
-          this.game.initime[oppIdx] = Date.now();
           this.re_setClocks();
           break;
         }
@@ -734,45 +732,43 @@ export default {
       );
     },
     sendLastate: function(target) {
-      if (
-        (this.game.moves.length > 0 && this.vr.turn != this.game.mycolor) ||
-        this.game.score != "*" ||
-        this.drawOffer == "sent" ||
-        this.rematchOffer == "sent"
-      ) {
-        // Send our "last state" informations to opponent
-        const L = this.game.moves.length;
-        const myIdx = ["w", "b"].indexOf(this.game.mycolor);
-        const myLastate = {
-          lastMove: L > 0 ? this.game.moves[L - 1] : undefined,
-          clock: this.game.clocks[myIdx],
-          // Since we played a move (or abort or resign),
-          // only drawOffer=="sent" is possible
-          drawSent: this.drawOffer == "sent",
-          rematchSent: this.rematchOffer == "sent",
-          score: this.game.score,
-          scoreMsg: this.game.scoreMsg,
-          movesCount: L,
-          initime: this.game.initime[1 - myIdx] //relevant only if I played
-        };
-        this.send("lastate", { data: myLastate, target: target });
-      } else {
-        this.send("lastate", { data: {nothing: true}, target: target });
-      }
+      // Send our "last state" informations to opponent
+      const L = this.game.moves.length;
+      const myIdx = ["w", "b"].indexOf(this.game.mycolor);
+      const myLastate = {
+        lastMove:
+          (L > 0 && this.vr.turn != this.game.mycolor)
+            ? this.game.moves[L - 1]
+            : undefined,
+        clock: this.game.clocks[myIdx],
+        // Since we played a move (or abort or resign),
+        // only drawOffer=="sent" is possible
+        drawSent: this.drawOffer == "sent",
+        rematchSent: this.rematchOffer == "sent",
+        score: this.game.score != "*" ? this.game.score : undefined,
+        scoreMsg: this.game.score != "*" ? this.game.scoreMsg : undefined,
+        movesCount: L
+      };
+      this.send("lastate", { data: myLastate, target: target });
     },
     // lastate was received, but maybe game wasn't ready yet:
     processLastate: function() {
       const data = this.lastate;
       this.lastate = undefined; //security...
       const L = this.game.moves.length;
+      const oppIdx = 1 - ["w", "b"].indexOf(this.game.mycolor);
+      this.game.clocks[oppIdx] = data.clock;
       if (data.movesCount > L) {
         // Just got last move from him
         this.$refs["basegame"].play(data.lastMove, "received", null, true);
-        this.processMove(data.lastMove, { clock: data.clock });
+        this.processMove(data.lastMove);
+      } else {
+        clearInterval(this.clockUpdate);
+        this.re_setClocks();
       }
       if (data.drawSent) this.drawOffer = "received";
       if (data.rematchSent) this.rematchOffer = "received";
-      if (data.score != "*") {
+      if (!!data.score) {
         this.drawOffer = "";
         if (this.game.score == "*")
           this.gameOver(data.score, data.scoreMsg);
@@ -817,7 +813,6 @@ export default {
           // Game state (including FEN): will be updated
           moves: [],
           clocks: [-1, -1], //-1 = unstarted
-          initime: [0, 0], //initialized later
           score: "*"
         }
       );
@@ -913,16 +908,16 @@ export default {
       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
+        // NOTE: clocks in seconds
         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.
+          // Adjust clocks
+          if (L >= 2) {
+            game.clocks[L % 2] -=
+              (Date.now() - game.moves[L-1].played) / 1000;
+          }
         }
         // Sort chat messages from newest to oldest
         game.chats.sort((c1, c2) => {
@@ -948,16 +943,20 @@ export default {
         // 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 (gtype == "live") {
+        if (game.clocks[0] < 0) {
+          // Game is unstarted. clock is ignored until move 2
+          game.clocks = [tc.mainTime, tc.mainTime];
+          if (myIdx >= 0) {
+            // I play in this live game
+            GameStorage.update(game.id, {
+              clocks: game.clocks
+            });
+          }
+        } else {
+          if (!!game.initime)
+            // It's my turn: clocks not updated yet
+            game.clocks[myIdx] -= (Date.now() - game.initime) / 1000;
         }
       }
       // TODO: merge next 2 "if" conditions
@@ -1075,41 +1074,34 @@ export default {
         GameStorage.get(this.gameRef, callback);
     },
     re_setClocks: function() {
+      this.virtualClocks = this.game.clocks.map(s => ppt(s).split(':'));
       if (this.game.moves.length < 2 || this.game.score != "*") {
         // 1st move not completed yet, or game over: freeze time
-        this.virtualClocks = this.game.clocks.map(s => ppt(s).split(':'));
         return;
       }
       const currentTurn = this.vr.turn;
       const currentMovesCount = this.game.moves.length;
       const colorIdx = ["w", "b"].indexOf(currentTurn);
-      let countdown =
-        this.game.clocks[colorIdx] -
-        (Date.now() - this.game.initime[colorIdx]) / 1000;
-      this.virtualClocks = [0, 1].map(i => {
-        const removeTime =
-          i == colorIdx ? (Date.now() - this.game.initime[colorIdx]) / 1000 : 0;
-        return ppt(this.game.clocks[i] - removeTime).split(':');
-      });
       this.clockUpdate = setInterval(
         () => {
           if (
-            countdown < 0 ||
+            this.game.clocks[colorIdx] < 0 ||
             this.game.moves.length > currentMovesCount ||
             this.game.score != "*"
           ) {
             clearInterval(this.clockUpdate);
-            if (countdown < 0)
+            if (this.game.clocks[colorIdx] < 0)
               this.gameOver(
                 currentTurn == "w" ? "0-1" : "1-0",
                 "Time"
               );
-          } else
+          } else {
             this.$set(
               this.virtualClocks,
               colorIdx,
-              ppt(Math.max(0, --countdown)).split(':')
+              ppt(Math.max(0, --this.game.clocks[colorIdx])).split(':')
             );
+          }
         },
         1000
       );
@@ -1122,21 +1114,28 @@ export default {
       const nextIdx = 1 - colorIdx;
       const doProcessMove = () => {
         const origMovescount = this.game.moves.length;
-        let addTime = 0; //for live games
+        // The move is (about to be) played: stop clock
+        clearInterval(this.clockUpdate);
         if (moveCol == this.game.mycolor && !data.receiveMyMove) {
           if (this.drawOffer == "received")
             // I refuse draw
             this.drawOffer = "";
           if (this.game.type == "live" && origMovescount >= 2) {
-            const elapsed = Date.now() - this.game.initime[colorIdx];
-            // elapsed time is measured in milliseconds
-            addTime = this.game.increment - elapsed / 1000;
+            this.game.clocks[colorIdx] += this.game.increment;
+            // For a correct display in casqe of disconnected opponent:
+            this.$set(
+              this.virtualClocks,
+              colorIdx,
+              ppt(this.game.clocks[colorIdx]).split(':')
+            );
+            GameStorage.update(this.gameRef, {
+              // It's not my turn anymore:
+              initime: null
+            });
           }
         }
         // Update current game object:
         playMove(move, this.vr);
-        // The move is played: stop clock
-        clearInterval(this.clockUpdate);
         if (!data.score)
           // Received move, score is computed in BaseGame, but maybe not yet.
           // ==> Compute it here, although this is redundant (TODO)
@@ -1144,21 +1143,10 @@ export default {
         if (data.score != "*") this.gameOver(data.score);
         this.game.moves.push(move);
         this.game.fen = this.vr.getFen();
-        if (this.game.type == "live") {
-          if (!!data.clock) this.game.clocks[colorIdx] = data.clock;
-          else this.game.clocks[colorIdx] += addTime;
-        } else {
+        if (this.game.type == "corr") {
           // In corr games, just reset clock to mainTime:
           this.game.clocks[colorIdx] = extractTime(this.game.cadence).mainTime;
         }
-        // NOTE: opponent's initime is reset after "gotmove" is received
-        if (
-          !this.game.mycolor ||
-          moveCol != this.game.mycolor ||
-          !!data.receiveMyMove
-        ) {
-          this.game.initime[nextIdx] = Date.now();
-        }
         // If repetition detected, consider that a draw offer was received:
         const fenObj = this.vr.getFenForRepeat();
         this.repeat[fenObj] =
@@ -1183,6 +1171,19 @@ export default {
         }
         // Since corr games are stored at only one location, update should be
         // done only by one player for each move:
+        if (
+          this.game.type == "live" &&
+          !!this.game.mycolor &&
+          moveCol != this.game.mycolor &&
+          this.game.moves.length >= 2
+        ) {
+          // Receive a move: update initime
+          this.game.initime = Date.now();
+          GameStorage.update(this.gameRef, {
+            // It's my turn now!
+            initime: this.game.initime
+          });
+        }
         if (
           !!this.game.mycolor &&
           !data.receiveMyMove &&
@@ -1219,7 +1220,6 @@ export default {
                 move: filtered_move,
                 moveIdx: origMovescount,
                 clocks: this.game.clocks,
-                initime: this.game.initime,
                 drawOffer: drawCode
               });
             };
@@ -1288,10 +1288,7 @@ export default {
             // The board might have been hidden:
             if (boardDiv.style.visibility == "hidden")
               boardDiv.style.visibility = "visible";
-            if (data.score == "*") {
-              this.game.initime[nextIdx] = Date.now();
-              this.re_setClocks();
-            }
+            if (data.score == "*") this.re_setClocks();
           }
         };
         let el = document.querySelector("#buttonsConfirm > .acceptBtn");
diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue
index 4d830bbf..d0cabb46 100644
--- a/client/src/views/Hall.vue
+++ b/client/src/views/Hall.vue
@@ -296,8 +296,8 @@ export default {
       encodeURIComponent(this.$route.path);
     this.conn = new WebSocket(this.connexionString);
     this.conn.onopen = connectAndPoll;
-    this.conn.onmessage = this.socketMessageListener;
-    this.conn.onclose = this.socketCloseListener;
+    this.conn.addEventListener("message", this.socketMessageListener);
+    this.conn.addEventListener("close", this.socketCloseListener);
   },
   mounted: function() {
     document.addEventListener('visibilitychange', this.visibilityChange);
@@ -653,6 +653,8 @@ export default {
           break;
         case "killed":
           // I logged in elsewhere:
+          this.conn.removeEventListener("message", this.socketMessageListener);
+          this.conn.removeEventListener("close", this.socketCloseListener);
           this.conn = null;
           alert(this.st.tr["New connexion detected: tab now offline"]);
           break;
@@ -1190,7 +1192,6 @@ export default {
           // Game state (including FEN): will be updated
           moves: [],
           clocks: [-1, -1], //-1 = unstarted
-          initime: [0, 0], //initialized later
           score: "*"
         }
       );
diff --git a/server/db/populate.sql b/server/db/populate.sql
index 2107afb9..d6566535 100644
--- a/server/db/populate.sql
+++ b/server/db/populate.sql
@@ -17,6 +17,7 @@ insert or ignore into Variants (name,description) values
   ('Checkered', 'Shared pieces'),
   ('Chess960', 'Standard rules'),
   ('Circular', 'Run forward'),
+  ('Coregal', 'Two royal pieces'),
   ('Crazyhouse', 'Captures reborn'),
   ('Cylinder', 'Neverending rows'),
   ('Dark', 'In the shadow'),