Revert to news button in red, no blinking. Add autoplay option
[vchess.git] / client / src / views / Game.vue
index 703ba18..ddbd330 100644 (file)
@@ -7,10 +7,14 @@ main
   )
     .card.text-center
       label.modal-close(for="modalInfo")
-      p(v-html="infoMessage")
+      a(
+        :href="'#/game/' + rematchId"
+        onClick="document.getElementById('modalInfo').checked=false"
+      )
+        | {{ st.tr["Rematch in progress"] }}
   input#modalChat.modal(
     type="checkbox"
-    @click="resetChatColor()"
+    @click="toggleChat()"
   )
   div#chatWrap(
     role="dialog"
@@ -33,7 +37,6 @@ main
         ref="chatcomp"
         :players="game.players"
         :pastChats="game.chats"
-        :newChat="newChat"
         @mychat="processChat"
         @chatcleared="clearChat"
       )
@@ -150,25 +153,21 @@ export default {
   data: function() {
     return {
       st: store.state,
-      gameRef: {
-        // rid = remote (socket) ID
-        id: "",
-        rid: ""
-      },
+      // gameRef can point to a corr game, local game or remote live game
+      gameRef: "",
       nextIds: [],
       game: {}, //passed to BaseGame
       // virtualClocks will be initialized from true game.clocks
       virtualClocks: [],
       vr: null, //"variant rules" object initialized from FEN
       drawOffer: "",
-      infoMessage: "",
+      rematchId: "",
       rematchOffer: "",
       lastateAsked: false,
       people: {}, //players + observers
       lastate: undefined, //used if opponent send lastate before game is ready
       repeat: {}, //detect position repetition
       curDiag: "", //for corr moves confirmation
-      newChat: "",
       conn: null,
       roomInitialized: false,
       // If newmove has wrong index: ask fullgame again:
@@ -201,13 +200,9 @@ export default {
           // In case of incomplete information variant:
           boardDiv.style.visibility = "hidden";
         this.atCreation();
-      } else {
+      } else
         // Same game ID
-        this.gameRef.id = to.params["id"];
-        this.gameRef.rid = to.query["rid"];
         this.nextIds = JSON.parse(this.$route.query["next"] || "[]");
-        this.loadGame();
-      }
     }
   },
   // NOTE: some redundant code with Hall.vue (mostly related to people array)
@@ -216,9 +211,10 @@ export default {
   },
   mounted: function() {
     document.addEventListener('visibilitychange', this.visibilityChange);
-    document
-      .getElementById("chatWrap")
-      .addEventListener("click", processModalClick);
+    ["chatWrap", "infoDiv"].forEach(eltName => {
+      document.getElementById(eltName)
+        .addEventListener("click", processModalClick);
+    });
     if ("ontouchstart" in window) {
       // Disable tooltips on smartphones:
       document.querySelectorAll("#aboveBoard .tooltip").forEach(elt => {
@@ -241,11 +237,8 @@ export default {
     },
     atCreation: function() {
       // 0] (Re)Set variables
-      this.gameRef.id = this.$route.params["id"];
-      // rid = remote ID to find an observed live game,
-      // next = next corr games IDs to navigate faster
-      // (Both might be undefined)
-      this.gameRef.rid = this.$route.query["rid"];
+      this.gameRef = this.$route.params["id"];
+      // next = next corr games IDs to navigate faster (if applicable)
       this.nextIds = JSON.parse(this.$route.query["next"] || "[]");
       // Always add myself to players' list
       const my = this.st.user;
@@ -271,7 +264,6 @@ export default {
       this.lastateAsked = false;
       this.rematchOffer = "";
       this.lastate = undefined;
-      this.newChat = "";
       this.roomInitialized = false;
       this.askGameTime = 0;
       this.gameIsLoading = false;
@@ -296,27 +288,27 @@ 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
           // Socket not ready yet (initial loading)
-          // NOTE: it's important to call callback without arguments,
-          // otherwise first arg is Websocket object and loadGame fails.
+          // NOTE: first arg is Websocket object, unused here:
           this.conn.onopen = () => callback();
       };
-      if (!this.gameRef.rid)
-        // Game stored locally or on server
-        this.loadGame(null, () => socketInit(this.roomInit));
-      else
-        // Game stored remotely: need socket to retrieve it
-        // NOTE: the callback "roomInit" will be lost, so we don't provide it.
-        // --> It will be given when receiving "fullgame" socket event.
-        socketInit(this.loadGame);
+      this.fetchGame((game) => {
+        if (!!game)
+          this.loadVariantThenGame(game, () => socketInit(this.roomInit));
+        else
+          // Live game stored remotely: need socket to retrieve it
+          // NOTE: the callback "roomInit" will be lost, so we don't provide it.
+          // --> It will be given when receiving "fullgame" socket event.
+          socketInit(() => { this.send("askfullgame"); });
+      });
     },
     cleanBeforeDestroy: function() {
       if (!!this.askLastate)
@@ -345,7 +337,7 @@ export default {
     isConnected: function(index) {
       const player = this.game.players[index];
       // Is it me ? In this case no need to bother with focus
-      if (this.st.user.sid == player.sid || this.st.user.id == player.uid)
+      if (this.st.user.sid == player.sid || this.st.user.id == player.id)
         // Still have to check for name (because of potential multi-accounts
         // on same browser, although this should be rare...)
         return (!this.st.user.name || this.st.user.name == player.name);
@@ -358,9 +350,9 @@ export default {
         )
         ||
         (
-          player.uid &&
+          !!player.id &&
           Object.values(this.people).some(p =>
-            p.id == player.uid && p.focus)
+            p.id == player.id && p.focus)
         )
       );
     },
@@ -375,8 +367,12 @@ export default {
       if (!!oppsid && !!this.people[oppsid]) return oppsid;
       return null;
     },
-    resetChatColor: function() {
-      // TODO: this is called twice, once on opening an once on closing
+    toggleChat: function() {
+      if (document.getElementById("modalChat").checked)
+        // Entering chat
+        document.getElementById("inputChat").focus();
+      // TODO: next line is only required when exiting chat,
+      // but the event for now isn't well detected.
       document.getElementById("chatBtn").classList.remove("somethingnew");
     },
     processChat: function(chat) {
@@ -408,7 +404,7 @@ export default {
         {
           data: data,
           targets: this.game.players.map(p => {
-            return { sid: p.sid, uid: p.uid };
+            return { sid: p.sid, id: p.id };
           })
         }
       );
@@ -426,13 +422,15 @@ export default {
       const currentUrl = document.location.href;
       const doAskGame = () => {
         if (document.location.href != currentUrl) return; //page change
-        if (!this.gameRef.rid)
-          // This is my game: just reload.
-          this.loadGame();
-        else
-          // Just ask fullgame again (once!), this is much simpler.
-          // If this fails, the user could just reload page :/
-          this.send("askfullgame", { target: this.gameRef.rid });
+        this.fetchGame((game) => {
+          if (!!game)
+            // This is my game: just reload.
+            this.loadGame(game);
+          else
+            // Just ask fullgame again (once!), this is much simpler.
+            // If this fails, the user could just reload page :/
+            this.send("askfullgame");
+        });
       };
       // Delay of at least 2s between two game requests
       const now = Date.now();
@@ -445,6 +443,8 @@ export default {
       const data = JSON.parse(msg.data);
       switch (data.code) {
         case "pollclients":
+          // TODO: shuffling and random filtering on server, if
+          // the room is really crowded.
           data.sockIds.forEach(sid => {
             if (sid != this.st.user.sid) {
               this.people[sid] = { focus: true };
@@ -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;
@@ -557,8 +559,7 @@ export default {
               players: this.game.players,
               vid: this.game.vid,
               cadence: this.game.cadence,
-              score: this.game.score,
-              rid: this.st.user.sid //useful in Hall if I'm an observer
+              score: this.game.score
             };
             this.send("game", { data: myGame, target: data.from });
           }
@@ -568,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) => {
@@ -581,7 +582,7 @@ export default {
           break;
         case "fullgame":
           // Callback "roomInit" to poll clients only after game is loaded
-          this.loadGame(data.data, this.roomInit);
+          this.loadVariantThenGame(data.data, this.roomInit);
           break;
         case "asklastate":
           // Sending informative last state if I played a move or score != "*"
@@ -592,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": {
@@ -633,16 +632,15 @@ export default {
                   !!this.game.mycolor &&
                   !receiveMyMove
                 ) {
-                  GameStorage.update(this.gameRef.id, { drawOffer: "" });
+                  GameStorage.update(this.gameRef, { drawOffer: "" });
                 }
               }
               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 }
               );
             }
           }
@@ -650,15 +648,14 @@ 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;
         }
         case "resign":
-          const score = data.side == "b" ? "1-0" : "0-1";
-          const side = data.side == "w" ? "White" : "Black";
+          const score = (data.data == "b" ? "1-0" : "0-1");
+          const side = (data.data == "w" ? "White" : "Black");
           this.gameOver(score, side + " surrender");
           break;
         case "abort":
@@ -670,10 +667,22 @@ export default {
         case "drawoffer":
           // NOTE: observers don't know who offered draw
           this.drawOffer = "received";
+          if (this.game.type == "live") {
+            GameStorage.update(
+              this.gameRef,
+              { drawOffer: V.GetOppCol(this.game.mycolor) }
+            );
+          }
           break;
         case "rematchoffer":
           // NOTE: observers don't know who offered rematch
           this.rematchOffer = data.data ? "received" : "";
+          if (this.game.type == "live") {
+            GameStorage.update(
+              this.gameRef,
+              { rematchOffer: V.GetOppCol(this.game.mycolor) }
+            );
+          }
           break;
         case "newgame": {
           // A game started, redirect if I'm playing in
@@ -686,34 +695,17 @@ export default {
             this.addAndGotoLiveGame(gameInfo);
           } else if (
             gameType == "corr" &&
-            gameInfo.players.some(p => p.uid == this.st.user.id)
+            gameInfo.players.some(p => p.id == this.st.user.id)
           ) {
             this.$router.push("/game/" + gameInfo.id);
           } else {
-            let urlRid = "";
-            if (gameInfo.cadence.indexOf('d') === -1) {
-              urlRid = "/?rid=";
-              // Select sid of any of the online players:
-              let onlineSid = [];
-              gameInfo.players.forEach(p => {
-                if (!!this.people[p.sid]) onlineSid.push(p.sid);
-              });
-              urlRid += onlineSid[Math.floor(Math.random() * onlineSid.length)];
-            }
-            this.infoMessage =
-              this.st.tr["Rematch in progress:"] +
-              " <a href='#/game/" +
-              gameInfo.id + urlRid +
-              "'>" +
-              "#/game/" +
-              gameInfo.id + urlRid +
-              "</a>";
+            this.rematchId = gameInfo.id;
             document.getElementById("modalInfo").checked = true;
           }
           break;
         }
         case "newchat":
-          this.newChat = data.data;
+          this.$refs["chatcomp"].newChat(data.data);
           if (!document.getElementById("modalChat").checked)
             document.getElementById("chatBtn").classList.add("somethingnew");
           break;
@@ -730,7 +722,7 @@ export default {
         "PUT",
         {
           data: {
-            gid: this.gameRef.id,
+            gid: this.gameRef,
             newObj: obj
           },
           success: () => {
@@ -740,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);
@@ -805,7 +795,7 @@ export default {
         this.send("drawoffer");
         if (this.game.type == "live") {
           GameStorage.update(
-            this.gameRef.id,
+            this.gameRef,
             { drawOffer: this.game.mycolor }
           );
         } else this.updateCorrGame({ drawOffer: this.game.mycolor });
@@ -823,7 +813,6 @@ export default {
           // Game state (including FEN): will be updated
           moves: [],
           clocks: [-1, -1], //-1 = unstarted
-          initime: [0, 0], //initialized later
           score: "*"
         }
       );
@@ -832,7 +821,7 @@ export default {
         if (!err) {
           if (this.st.settings.sound)
             new Audio("/sounds/newgame.flac").play().catch(() => {});
-          callback();
+          if (!!callback) callback();
           this.$router.push("/game/" + gameInfo.id);
         }
       });
@@ -849,8 +838,11 @@ export default {
           cadence: this.game.cadence
         };
         const notifyNewGame = () => {
-          let oppsid = this.getOppsid(); //may be null
+          const oppsid = this.getOppsid(); //may be null
           this.send("rnewgame", { data: gameInfo, oppsid: oppsid });
+          // To main Hall if corr game:
+          if (this.game.type == "corr")
+            this.send("newgame", { data: gameInfo });
           // Also to MyGames page:
           this.notifyMyGames("newgame", gameInfo);
         };
@@ -877,7 +869,7 @@ export default {
         this.send("rematchoffer", { data: true });
         if (this.game.type == "live") {
           GameStorage.update(
-            this.gameRef.id,
+            this.gameRef,
             { rematchOffer: this.game.mycolor }
           );
         } else this.updateCorrGame({ rematchOffer: this.game.mycolor });
@@ -887,7 +879,7 @@ export default {
         this.send("rematchoffer", { data: false });
         if (this.game.type == "live") {
           GameStorage.update(
-            this.gameRef.id,
+            this.gameRef,
             { rematchOffer: '' }
           );
         } else this.updateCorrGame({ rematchOffer: 'n' });
@@ -902,243 +894,214 @@ export default {
       if (!this.game.mycolor || !confirm(this.st.tr["Resign the game?"]))
         return;
       this.send("resign", { data: this.game.mycolor });
-      const score = this.game.mycolor == "w" ? "0-1" : "1-0";
-      const side = this.game.mycolor == "w" ? "White" : "Black";
+      const score = (this.game.mycolor == "w" ? "0-1" : "1-0");
+      const side = (this.game.mycolor == "w" ? "White" : "Black");
       this.gameOver(score, side + " surrender");
     },
-    // 3 cases for loading a game:
-    //  - from indexedDB (running or completed live game I play)
-    //  - from server (one correspondance game I play[ed] or not)
-    //  - from remote peer (one live game I don't play, finished or not)
     loadGame: function(game, callback) {
-      const afterRetrieval = async (game) => {
-        const vModule = await import("@/variants/" + game.vname + ".js");
-        window.V = vModule.VariantRules;
-        this.vr = new V(game.fen);
-        const gtype = this.getGameType(game);
-        const tc = extractTime(game.cadence);
-        const myIdx = game.players.findIndex(p => {
-          return p.sid == this.st.user.sid || p.uid == this.st.user.id;
-        });
-        const mycolor = [undefined, "w", "b"][myIdx + 1]; //undefined for observers
-        if (!game.chats) game.chats = []; //live games don't have chat history
-        if (gtype == "corr") {
-          if (game.players[0].color == "b") {
-            // Adopt the same convention for live and corr games: [0] = white
-            [game.players[0], game.players[1]] = [
-              game.players[1],
-              game.players[0]
-            ];
-          }
-          // NOTE: clocks in seconds, initime in milliseconds
-          game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of
-          game.clocks = [tc.mainTime, tc.mainTime];
-          const L = game.moves.length;
-          if (game.score == "*") {
-            // Set clocks + initime
-            game.initime = [0, 0];
-            if (L >= 1) {
-              const gameLastupdate = game.moves[L-1].played;
-              game.initime[L % 2] = gameLastupdate;
-              if (L >= 2) {
-                game.clocks[L % 2] =
-                  tc.mainTime - (Date.now() - gameLastupdate) / 1000;
-              }
-            }
-          }
-          // Sort chat messages from newest to oldest
-          game.chats.sort((c1, c2) => {
-            return c2.added - c1.added;
-          });
-          if (myIdx >= 0 && game.score == "*" && game.chats.length > 0) {
-            // Did a chat message arrive after my last move?
-            let dtLastMove = 0;
-            if (L == 1 && myIdx == 0)
-              dtLastMove = game.moves[0].played;
-            else if (L >= 2) {
-              if (L % 2 == 0) {
-                // It's now white turn
-                dtLastMove = game.moves[L-1-(1-myIdx)].played;
-              } else {
-                // Black turn:
-                dtLastMove = game.moves[L-1-myIdx].played;
-              }
-            }
-            if (dtLastMove < game.chats[0].added)
-              document.getElementById("chatBtn").classList.add("somethingnew");
+      this.vr = new V(game.fen);
+      const gtype = this.getGameType(game);
+      const tc = extractTime(game.cadence);
+      const myIdx = game.players.findIndex(p => {
+        return p.sid == this.st.user.sid || p.id == this.st.user.id;
+      });
+      const mycolor = [undefined, "w", "b"][myIdx + 1]; //undefined for observers
+      if (!game.chats) game.chats = []; //live games don't have chat history
+      if (gtype == "corr") {
+        // NOTE: clocks in seconds
+        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 == "*") {
+          // Adjust clocks
+          if (L >= 2) {
+            game.clocks[L % 2] -=
+              (Date.now() - game.moves[L-1].played) / 1000;
           }
-          // 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
-          game.clocks = [tc.mainTime, tc.mainTime];
-          if (game.score == "*") {
-            game.initime[0] = Date.now();
-            if (myIdx >= 0) {
-              // I play in this live game; corr games don't have clocks+initime
-              GameStorage.update(game.id, {
-                clocks: game.clocks,
-                initime: game.initime
-              });
+        // Sort chat messages from newest to oldest
+        game.chats.sort((c1, c2) => {
+          return c2.added - c1.added;
+        });
+        if (myIdx >= 0 && game.score == "*" && game.chats.length > 0) {
+          // Did a chat message arrive after my last move?
+          let dtLastMove = 0;
+          if (L == 1 && myIdx == 0)
+            dtLastMove = game.moves[0].played;
+          else if (L >= 2) {
+            if (L % 2 == 0) {
+              // It's now white turn
+              dtLastMove = game.moves[L-1-(1-myIdx)].played;
+            } else {
+              // Black turn:
+              dtLastMove = game.moves[L-1-myIdx].played;
             }
           }
+          if (dtLastMove < game.chats[0].added)
+            document.getElementById("chatBtn").classList.add("somethingnew");
         }
-        // TODO: merge next 2 "if" conditions
-        if (!!game.drawOffer) {
-          if (game.drawOffer == "t")
-            // Three repetitions
-            this.drawOffer = "threerep";
-          else {
-            // Draw offered by any of the players:
-            if (myIdx < 0) this.drawOffer = "received";
-            else {
-              // I play in this game:
-              if (
-                (game.drawOffer == "w" && myIdx == 0) ||
-                (game.drawOffer == "b" && myIdx == 1)
-              )
-                this.drawOffer = "sent";
-              else this.drawOffer = "received";
-            }
+        // 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") {
+        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;
         }
-        if (!!game.rematchOffer) {
-          if (myIdx < 0) this.rematchOffer = "received";
+      }
+      // TODO: merge next 2 "if" conditions
+      if (!!game.drawOffer) {
+        if (game.drawOffer == "t")
+          // Three repetitions
+          this.drawOffer = "threerep";
+        else {
+          // Draw offered by any of the players:
+          if (myIdx < 0) this.drawOffer = "received";
           else {
             // I play in this game:
             if (
-              (game.rematchOffer == "w" && myIdx == 0) ||
-              (game.rematchOffer == "b" && myIdx == 1)
+              (game.drawOffer == "w" && myIdx == 0) ||
+              (game.drawOffer == "b" && myIdx == 1)
             )
-              this.rematchOffer = "sent";
-            else this.rematchOffer = "received";
+              this.drawOffer = "sent";
+            else this.drawOffer = "received";
           }
         }
-        this.repeat = {}; //reset: scan past moves' FEN:
-        let repIdx = 0;
-        let vr_tmp = new V(game.fenStart);
-        let curTurn = "n";
-        game.moves.forEach(m => {
-          playMove(m, vr_tmp);
-          const fenIdx = vr_tmp.getFen().replace(/ /g, "_");
-          this.repeat[fenIdx] = this.repeat[fenIdx]
-            ? this.repeat[fenIdx] + 1
-            : 1;
-        });
-        if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep";
-        this.game = Object.assign(
-          // NOTE: assign mycolor here, since BaseGame could also be VS computer
-          {
-            type: gtype,
-            increment: tc.increment,
-            mycolor: mycolor,
-            // opponent sid not strictly required (or available), but easier
-            // at least oppsid or oppid is available anyway:
-            oppsid: myIdx < 0 ? undefined : game.players[1 - myIdx].sid,
-            oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].uid
-          },
-          game,
-        );
-        if (this.gameIsLoading)
-          // Re-load game because we missed some moves:
-          // artificially reset BaseGame (required if moves arrived in wrong order)
-          this.$refs["basegame"].re_setVariables();
+      }
+      if (!!game.rematchOffer) {
+        if (myIdx < 0) this.rematchOffer = "received";
         else {
-          // Initial loading:
-          this.gotMoveIdx = game.moves.length - 1;
-          // If we arrive here after 'nextGame' action, the board might be hidden
-          let boardDiv = document.querySelector(".game");
-          if (!!boardDiv && boardDiv.style.visibility == "hidden")
-            boardDiv.style.visibility = "visible";
-        }
-        this.re_setClocks();
-        this.$nextTick(() => {
-          this.game.rendered = true;
-          // Did lastate arrive before game was rendered?
-          if (this.lastate) this.processLastate();
-        });
-        if (this.lastateAsked) {
-          this.lastateAsked = false;
-          this.sendLastate(game.oppsid);
-        }
-        if (this.gameIsLoading) {
-          this.gameIsLoading = false;
-          if (this.gotMoveIdx >= game.moves.length)
-            // Some moves arrived meanwhile...
-            this.askGameAgain();
+          // I play in this game:
+          if (
+            (game.rematchOffer == "w" && myIdx == 0) ||
+            (game.rematchOffer == "b" && myIdx == 1)
+          )
+            this.rematchOffer = "sent";
+          else this.rematchOffer = "received";
         }
-        if (!!callback) callback();
-      };
-      if (!!game) {
-        afterRetrieval(game);
-        return;
       }
-      if (this.gameRef.rid) {
-        // Remote live game: forgetting about callback func... (TODO: design)
-        this.send("askfullgame", { target: this.gameRef.rid });
-      } else {
-        // Local or corr game on server.
-        // NOTE: afterRetrieval() is never called if game not found
-        const gid = this.gameRef.id;
-        if (Number.isInteger(gid) || !isNaN(parseInt(gid))) {
-          // corr games identifiers are integers
-          ajax(
-            "/games",
-            "GET",
-            {
-              data: { gid: gid },
-              success: (res) => {
-                let g = res.game;
-                g.moves.forEach(m => {
-                  m.squares = JSON.parse(m.squares);
-                });
-                afterRetrieval(g);
-              }
-            }
-          );
-        }
-        else
-          // Local game
-          GameStorage.get(this.gameRef.id, afterRetrieval);
+      this.repeat = {}; //reset: scan past moves' FEN:
+      let repIdx = 0;
+      let vr_tmp = new V(game.fenStart);
+      let curTurn = "n";
+      game.moves.forEach(m => {
+        playMove(m, vr_tmp);
+        const fenIdx = vr_tmp.getFen().replace(/ /g, "_");
+        this.repeat[fenIdx] = this.repeat[fenIdx]
+          ? this.repeat[fenIdx] + 1
+          : 1;
+      });
+      if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep";
+      this.game = Object.assign(
+        // NOTE: assign mycolor here, since BaseGame could also be VS computer
+        {
+          type: gtype,
+          increment: tc.increment,
+          mycolor: mycolor,
+          // opponent sid not strictly required (or available), but easier
+          // at least oppsid or oppid is available anyway:
+          oppsid: myIdx < 0 ? undefined : game.players[1 - myIdx].sid,
+          oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].id
+        },
+        game
+      );
+      this.$refs["basegame"].re_setVariables(this.game);
+      if (!this.gameIsLoading) {
+        // Initial loading:
+        this.gotMoveIdx = game.moves.length - 1;
+        // If we arrive here after 'nextGame' action, the board might be hidden
+        let boardDiv = document.querySelector(".game");
+        if (!!boardDiv && boardDiv.style.visibility == "hidden")
+          boardDiv.style.visibility = "visible";
       }
+      this.re_setClocks();
+      this.$nextTick(() => {
+        this.game.rendered = true;
+        // Did lastate arrive before game was rendered?
+        if (this.lastate) this.processLastate();
+      });
+      if (this.lastateAsked) {
+        this.lastateAsked = false;
+        this.sendLastate(game.oppsid);
+      }
+      if (this.gameIsLoading) {
+        this.gameIsLoading = false;
+        if (this.gotMoveIdx >= game.moves.length)
+          // Some moves arrived meanwhile...
+          this.askGameAgain();
+      }
+      if (!!callback) callback();
+    },
+    loadVariantThenGame: async function(game, callback) {
+      await import("@/variants/" + game.vname + ".js")
+      .then((vModule) => {
+        window.V = vModule[game.vname + "Rules"];
+        this.loadGame(game, callback);
+      });
+    },
+    // 3 cases for loading a game:
+    //  - from indexedDB (running or completed live game I play)
+    //  - from server (one correspondance game I play[ed] or not)
+    //  - from remote peer (one live game I don't play, finished or not)
+    fetchGame: function(callback) {
+      if (Number.isInteger(this.gameRef) || !isNaN(parseInt(this.gameRef))) {
+        // corr games identifiers are integers
+        ajax(
+          "/games",
+          "GET",
+          {
+            data: { gid: this.gameRef },
+            success: (res) => {
+              res.game.moves.forEach(m => {
+                m.squares = JSON.parse(m.squares);
+              });
+              callback(res.game);
+            }
+          }
+        );
+      } else
+        // Local game (or live remote)
+        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
       );
@@ -1147,48 +1110,43 @@ export default {
     processMove: function(move, data) {
       if (!data) data = {};
       const moveCol = this.vr.turn;
+      const colorIdx = ["w", "b"].indexOf(moveCol);
+      const nextIdx = 1 - colorIdx;
       const doProcessMove = () => {
-        const colorIdx = ["w", "b"].indexOf(moveCol);
-        const nextIdx = 1 - colorIdx;
         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 has not been computed in BaseGame (!!noemit)
-          const score = this.vr.getCurrentScore();
-          if (score != "*") this.gameOver(score);
-        }
+        if (!data.score)
+          // Received move, score is computed in BaseGame, but maybe not yet.
+          // ==> Compute it here, although this is redundant (TODO)
+          data.score = this.vr.getCurrentScore();
+        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;
-        }
-        // In corr games, just reset clock to mainTime:
-        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] =
@@ -1206,13 +1164,26 @@ export default {
           this.notifyMyGames(
             "turn",
             {
-              gid: this.gameRef.id,
+              gid: this.gameRef,
               turn: this.vr.turn
             }
           );
         }
         // 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 &&
@@ -1236,7 +1207,6 @@ export default {
               fen: this.game.fen,
               move: {
                 squares: filtered_move,
-                played: Date.now(),
                 idx: origMovescount
               },
               // Code "n" for "None" to force reset (otherwise it's ignored)
@@ -1245,12 +1215,11 @@ export default {
           }
           else {
             const updateStorage = () => {
-              GameStorage.update(this.gameRef.id, {
+              GameStorage.update(this.gameRef, {
                 fen: this.game.fen,
                 move: filtered_move,
                 moveIdx: origMovescount,
                 clocks: this.game.clocks,
-                initime: this.game.initime,
                 drawOffer: drawCode
               });
             };
@@ -1271,6 +1240,7 @@ export default {
           };
           if (this.game.type == "live")
             sendMove["clock"] = this.game.clocks[colorIdx];
+          // (Live) Clocks will re-start when the opponent pingback arrive
           this.opponentGotMove = false;
           this.send("newmove", {data: sendMove});
           // If the opponent doesn't reply gotmove soon enough, re-send move:
@@ -1318,6 +1288,7 @@ export default {
             // The board might have been hidden:
             if (boardDiv.style.visibility == "hidden")
               boardDiv.style.visibility = "visible";
+            if (data.score == "*") this.re_setClocks();
           }
         };
         let el = document.querySelector("#buttonsConfirm > .acceptBtn");
@@ -1374,7 +1345,7 @@ export default {
       this.game.scoreMsg = scoreMsg;
       this.$set(this.game, "scoreMsg", scoreMsg);
       const myIdx = this.game.players.findIndex(p => {
-        return p.sid == this.st.user.sid || p.uid == this.st.user.id;
+        return p.sid == this.st.user.sid || p.id == this.st.user.id;
       });
       if (myIdx >= 0) {
         // OK, I play in this game
@@ -1383,7 +1354,7 @@ export default {
           scoreMsg: scoreMsg
         };
         if (this.game.type == "live") {
-          GameStorage.update(this.gameRef.id, scoreObj);
+          GameStorage.update(this.gameRef, scoreObj);
           if (!!callback) callback();
         }
         else this.updateCorrGame(scoreObj, callback);
@@ -1393,7 +1364,7 @@ export default {
         this.notifyMyGames(
           "score",
           {
-            gid: this.gameRef.id,
+            gid: this.gameRef,
             score: score
           }
         );
@@ -1438,7 +1409,7 @@ button
   margin: 0
   display: inline-flex
   img
-    height: 24px
+    height: 22px
     display: flex
     @media screen and (max-width: 767px)
       height: 18px