Fix message when resigning
[vchess.git] / client / src / views / Game.vue
index d62e2ca..5785abd 100644 (file)
@@ -1,8 +1,20 @@
 <template lang="pug">
 main
+  input#modalInfo.modal(type="checkbox")
+  div#infoDiv(
+    role="dialog"
+    data-checkbox="modalInfo"
+  )
+    .card.text-center
+      label.modal-close(for="modalInfo")
+      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"
@@ -11,7 +23,7 @@ main
     .card
       label.modal-close(for="modalChat")
       #participants
-        span {{ Object.keys(people).length + " " + st.tr["participant(s):"] }} 
+        span {{ st.tr["Participant(s):"] }} 
         span(
           v-for="p in Object.values(people)"
           v-if="p.focus && !!p.name"
@@ -25,7 +37,6 @@ main
         ref="chatcomp"
         :players="game.players"
         :pastChats="game.chats"
-        :newChat="newChat"
         @mychat="processChat"
         @chatcleared="clearChat"
       )
@@ -81,8 +92,9 @@ main
         )
           img(src="/images/icons/resign.svg")
       button.tooltip(
-        v-else-if="!!game.mycolor"
-        @click="rematch()"
+        v-else
+        @click="clickRematch()"
+        :class="{['rematch-' + rematchOffer]: true}"
         :aria-label="st.tr['Rematch']"
       )
         img(src="/images/icons/rematch.svg")
@@ -152,12 +164,13 @@ export default {
       virtualClocks: [],
       vr: null, //"variant rules" object initialized from FEN
       drawOffer: "",
+      rematchId: "",
+      rematchOffer: "",
+      lastateAsked: false,
       people: {}, //players + observers
-      onMygames: [], //opponents (or me) on "MyGames" page
       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:
@@ -195,7 +208,7 @@ export default {
         this.gameRef.id = to.params["id"];
         this.gameRef.rid = to.query["rid"];
         this.nextIds = JSON.parse(this.$route.query["next"] || "[]");
-        this.loadGame();
+        this.fetchGame();
       }
     }
   },
@@ -205,12 +218,13 @@ 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.getElementsByClassName("tooltip").forEach(elt => {
+      document.querySelectorAll("#aboveBoard .tooltip").forEach(elt => {
         elt.classList.remove("tooltip");
       });
     }
@@ -257,9 +271,9 @@ export default {
       this.virtualClocks = [[0,0], [0,0]];
       this.vr = null;
       this.drawOffer = "";
-      this.onMygames = [];
+      this.lastateAsked = false;
+      this.rematchOffer = "";
       this.lastate = undefined;
-      this.newChat = "";
       this.roomInitialized = false;
       this.askGameTime = 0;
       this.gameIsLoading = false;
@@ -276,6 +290,8 @@ export default {
         params.socketUrl +
         "/?sid=" +
         this.st.user.sid +
+        "&id=" +
+        this.st.user.id +
         "&tmpId=" +
         getRandString() +
         "&page=" +
@@ -292,17 +308,17 @@ export default {
         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.
+          // otherwise first arg is Websocket object and fetchGame fails.
           this.conn.onopen = () => callback();
       };
       if (!this.gameRef.rid)
         // Game stored locally or on server
-        this.loadGame(null, () => socketInit(this.roomInit));
+        this.fetchGame(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);
+        socketInit(this.fetchGame);
     },
     cleanBeforeDestroy: function() {
       if (!!this.askLastate)
@@ -331,7 +347,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);
@@ -344,14 +360,29 @@ export default {
         )
         ||
         (
-          player.uid &&
+          !!player.id &&
           Object.values(this.people).some(p =>
-            p.id == player.uid && p.focus)
+            p.id == player.id && p.focus)
         )
       );
     },
-    resetChatColor: function() {
-      // TODO: this is called twice, once on opening an once on closing
+    getOppsid: function() {
+      let oppsid = this.game.oppsid;
+      if (!oppsid) {
+        oppsid = Object.keys(this.people).find(
+          sid => this.people[sid].id == this.game.oppid
+        );
+      }
+      // oppsid is useful only if opponent is online:
+      if (!!oppsid && !!this.people[oppsid]) return oppsid;
+      return null;
+    },
+    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) {
@@ -373,17 +404,20 @@ export default {
         this.$set(this.game, "chats", []);
       }
     },
-    // Notify turn after a new move (to opponent and me on MyGames page)
-    notifyTurn: function(sid) {
-      const player = this.people[sid];
-      const colorIdx = this.game.players.findIndex(
-        p => p.sid == sid || p.uid == player.id);
-      const color = ["w","b"][colorIdx];
-      const movesCount = this.game.moves.length;
-      const yourTurn =
-        (color == "w" && movesCount % 2 == 0) ||
-        (color == "b" && movesCount % 2 == 1);
-      this.send("turnchange", { target: sid, yourTurn: yourTurn });
+    getGameType: function(game) {
+      return game.cadence.indexOf("d") >= 0 ? "corr" : "live";
+    },
+    // Notify something after a new move (to opponent and me on MyGames page)
+    notifyMyGames: function(thing, data) {
+      this.send(
+        "notify" + thing,
+        {
+          data: data,
+          targets: this.game.players.map(p => {
+            return { sid: p.sid, id: p.id };
+          })
+        }
+      );
     },
     showNextGame: function() {
       // Did I play in current game? If not, add it to nextIds list
@@ -393,10 +427,6 @@ export default {
       this.$router.push(
         "/game/" + nextGid + "/?next=" + JSON.stringify(this.nextIds));
     },
-    rematch: function() {
-      alert("Unimplemented yet (soon :) )");
-      // TODO: same logic as for draw, but re-click remove rematch offer (toggle)
-    },
     askGameAgain: function() {
       this.gameIsLoading = true;
       const currentUrl = document.location.href;
@@ -404,7 +434,7 @@ export default {
         if (document.location.href != currentUrl) return; //page change
         if (!this.gameRef.rid)
           // This is my game: just reload.
-          this.loadGame();
+          this.fetchGame();
         else
           // Just ask fullgame again (once!), this is much simpler.
           // If this fails, the user could just reload page :/
@@ -438,22 +468,6 @@ export default {
         case "disconnect":
           this.$delete(this.people, data.from);
           break;
-        case "mconnect": {
-          // TODO: from MyGames page : send mconnect message with the list of gid (live and corr)
-          // Either me (another tab) or opponent
-          const sid = data.from;
-          if (!this.onMygames.some(s => s == sid))
-          {
-            this.onMygames.push(sid);
-            this.notifyTurn(sid); //TODO: this may require server ID (so, notify after receiving identity)
-          }
-          break;
-          if (!this.people[sid])
-            this.send("askidentity", { target: sid });
-        }
-        case "mdisconnect":
-          ArrayFun.remove(this.onMygames, sid => sid == data.from);
-          break;
         case "getfocus": {
           let player = this.people[data.from];
           if (!!player) {
@@ -560,7 +574,7 @@ export default {
             .filter(k =>
               [
                 "id","fen","players","vid","cadence","fenStart","vname",
-                "moves","clocks","initime","score","drawOffer"
+                "moves","clocks","initime","score","drawOffer","rematchOffer"
               ].includes(k))
             .reduce(
               (obj, k) => {
@@ -573,33 +587,13 @@ export default {
           break;
         case "fullgame":
           // Callback "roomInit" to poll clients only after game is loaded
-          this.loadGame(data.data, this.roomInit);
+          this.fetchGame(data.data, this.roomInit);
           break;
         case "asklastate":
           // Sending informative last state if I played a move or score != "*"
-          if (
-            (this.game.moves.length > 0 && this.vr.turn != this.game.mycolor) ||
-            this.game.score != "*" ||
-            this.drawOffer == "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",
-              score: this.game.score,
-              score: this.game.scoreMsg,
-              movesCount: L,
-              initime: this.game.initime[1 - myIdx] //relevant only if I played
-            };
-            this.send("lastate", { data: myLastate, target: data.from });
-          } else {
-            this.send("lastate", { data: {nothing: true}, target: data.from });
-          }
+          // If the game or moves aren't loaded yet, delay the sending:
+          if (!this.game || !this.game.moves) this.lastateAsked = true;
+          else this.sendLastate(data.from);
           break;
         case "lastate": {
           // Got opponent infos about last move
@@ -669,8 +663,8 @@ export default {
           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":
@@ -683,8 +677,42 @@ export default {
           // NOTE: observers don't know who offered draw
           this.drawOffer = "received";
           break;
+        case "rematchoffer":
+          // NOTE: observers don't know who offered rematch
+          this.rematchOffer = data.data ? "received" : "";
+          break;
+        case "newgame": {
+          // A game started, redirect if I'm playing in
+          const gameInfo = data.data;
+          const gameType = this.getGameType(gameInfo);
+          if (
+            gameType == "live" &&
+            gameInfo.players.some(p => p.sid == this.st.user.sid)
+          ) {
+            this.addAndGotoLiveGame(gameInfo);
+          } else if (
+            gameType == "corr" &&
+            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.rematchId = gameInfo.id + urlRid;
+            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;
@@ -710,6 +738,33 @@ 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 });
+      }
+    },
     // lastate was received, but maybe game wasn't ready yet:
     processLastate: function() {
       const data = this.lastate;
@@ -721,6 +776,7 @@ export default {
         this.processMove(data.lastMove, { clock: data.clock });
       }
       if (data.drawSent) this.drawOffer = "received";
+      if (data.rematchSent) this.rematchOffer = "received";
       if (data.score != "*") {
         this.drawOffer = "";
         if (this.game.score == "*")
@@ -754,6 +810,91 @@ export default {
         } else this.updateCorrGame({ drawOffer: this.game.mycolor });
       }
     },
+    addAndGotoLiveGame: function(gameInfo, callback) {
+      const game = Object.assign(
+        {},
+        gameInfo,
+        {
+          // (other) Game infos: constant
+          fenStart: gameInfo.fen,
+          vname: this.game.vname,
+          created: Date.now(),
+          // Game state (including FEN): will be updated
+          moves: [],
+          clocks: [-1, -1], //-1 = unstarted
+          initime: [0, 0], //initialized later
+          score: "*"
+        }
+      );
+      GameStorage.add(game, (err) => {
+        // No error expected.
+        if (!err) {
+          if (this.st.settings.sound)
+            new Audio("/sounds/newgame.flac").play().catch(() => {});
+          if (!!callback) callback();
+          this.$router.push("/game/" + gameInfo.id);
+        }
+      });
+    },
+    clickRematch: function() {
+      if (!this.game.mycolor) return; //I'm just spectator
+      if (this.rematchOffer == "received") {
+        // Start a new game!
+        let gameInfo = {
+          id: getRandString(), //ignored if corr
+          fen: V.GenRandInitFen(this.game.randomness),
+          players: this.game.players.reverse(),
+          vid: this.game.vid,
+          cadence: this.game.cadence
+        };
+        const notifyNewGame = () => {
+          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);
+        };
+        if (this.game.type == "live")
+          this.addAndGotoLiveGame(gameInfo, notifyNewGame);
+        else {
+          // corr game
+          ajax(
+            "/games",
+            "POST",
+            {
+              // cid is useful to delete the challenge:
+              data: { gameInfo: gameInfo },
+              success: (response) => {
+                gameInfo.id = response.gameId;
+                notifyNewGame();
+                this.$router.push("/game/" + response.gameId);
+              }
+            }
+          );
+        }
+      } else if (this.rematchOffer == "") {
+        this.rematchOffer = "sent";
+        this.send("rematchoffer", { data: true });
+        if (this.game.type == "live") {
+          GameStorage.update(
+            this.gameRef.id,
+            { rematchOffer: this.game.mycolor }
+          );
+        } else this.updateCorrGame({ rematchOffer: this.game.mycolor });
+      } else if (this.rematchOffer == "sent") {
+        // Toggle rematch offer (on --> off)
+        this.rematchOffer = "";
+        this.send("rematchoffer", { data: false });
+        if (this.game.type == "live") {
+          GameStorage.update(
+            this.gameRef.id,
+            { rematchOffer: '' }
+          );
+        } else this.updateCorrGame({ rematchOffer: 'n' });
+      }
+    },
     abortGame: function() {
       if (!this.game.mycolor || !confirm(this.st.tr["Terminate game?"])) return;
       this.gameOver("?", "Stop");
@@ -763,8 +904,8 @@ 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:
@@ -772,156 +913,161 @@ export default {
     //  - from server (one correspondance game I play[ed] or not)
     //  - from remote peer (one live game I don't play, finished or not)
     loadGame: function(game, callback) {
-      const afterRetrieval = async game => {
-        const vModule = await import("@/variants/" + game.vname + ".js");
-        window.V = vModule.VariantRules;
-        this.vr = new V(game.fen);
-        const gtype = game.cadence.indexOf("d") >= 0 ? "corr" : "live";
-        const tc = extractTime(game.cadence);
-        const myIdx = game.players.findIndex(p => {
-          return p.sid == this.st.user.sid || p.uid == this.st.user.id;
+      this.vr = new V(game.fen);
+      const gtype = this.getGameType(game);
+      const tc = extractTime(game.cadence);
+      const myIdx = game.players.findIndex(p => {
+        return p.sid == this.st.user.sid || p.id == this.st.user.id;
+      });
+      const mycolor = [undefined, "w", "b"][myIdx + 1]; //undefined for observers
+      if (!game.chats) game.chats = []; //live games don't have chat history
+      if (gtype == "corr") {
+        // NOTE: clocks in seconds, initime in milliseconds
+        game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of
+        game.clocks = [tc.mainTime, tc.mainTime];
+        const L = game.moves.length;
+        if (game.score == "*") {
+          // Set clocks + initime
+          game.initime = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
+          if (L >= 1) game.initime[L % 2] = game.moves[L-1].played;
+          // NOTE: game.clocks shouldn't be computed right now:
+          // job will be done in re_setClocks() called soon below.
+        }
+        // Sort chat messages from newest to oldest
+        game.chats.sort((c1, c2) => {
+          return c2.added - c1.added;
         });
-        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 (myIdx >= 0 && game.score == "*" && game.chats.length > 0) {
+          // Did a chat message arrive after my last move?
+          let dtLastMove = 0;
+          if (L == 1 && myIdx == 0)
+            dtLastMove = game.moves[0].played;
+          else if (L >= 2) {
+            if (L % 2 == 0) {
+              // It's now white turn
+              dtLastMove = game.moves[L-1-(1-myIdx)].played;
+            } else {
+              // Black turn:
+              dtLastMove = game.moves[L-1-myIdx].played;
             }
-            if (dtLastMove < game.chats[0].added)
-              document.getElementById("chatBtn").classList.add("somethingnew");
           }
-          // Now that we used idx and played, re-format moves as for live games
-          game.moves = game.moves.map(m => m.squares);
+          if (dtLastMove < game.chats[0].added)
+            document.getElementById("chatBtn").classList.add("somethingnew");
         }
-        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
-              });
-            }
-          }
+        // Now that we used idx and played, re-format moves as for live games
+        game.moves = game.moves.map(m => m.squares);
+      }
+      if (gtype == "live" && game.clocks[0] < 0) {
+        // Game is unstarted. clocks and initime are ignored until move 2
+        game.clocks = [tc.mainTime, tc.mainTime];
+        game.initime = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
+        if (myIdx >= 0) {
+          // I play in this live game
+          GameStorage.update(game.id, {
+            clocks: game.clocks,
+            initime: game.initime
+          });
         }
-        if (!!game.drawOffer) {
-          if (game.drawOffer == "t")
-            // Three repetitions
-            this.drawOffer = "threerep";
+      }
+      // 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 {
-            // 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";
-            }
+            // I play in this game:
+            if (
+              (game.drawOffer == "w" && myIdx == 0) ||
+              (game.drawOffer == "b" && myIdx == 1)
+            )
+              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";
+          // I play in this game:
+          if (
+            (game.rematchOffer == "w" && myIdx == 0) ||
+            (game.rematchOffer == "b" && myIdx == 1)
+          )
+            this.rematchOffer = "sent";
+          else this.rematchOffer = "received";
         }
-        this.re_setClocks();
-        this.$nextTick(() => {
-          this.game.rendered = true;
-          // Did lastate arrive before game was rendered?
-          if (this.lastate) this.processLastate();
+      }
+      this.repeat = {}; //reset: scan past moves' FEN:
+      let repIdx = 0;
+      let vr_tmp = new V(game.fenStart);
+      let curTurn = "n";
+      game.moves.forEach(m => {
+        playMove(m, vr_tmp);
+        const fenIdx = vr_tmp.getFen().replace(/ /g, "_");
+        this.repeat[fenIdx] = this.repeat[fenIdx]
+          ? this.repeat[fenIdx] + 1
+          : 1;
+      });
+      if (this.repeat[repIdx] >= 3) this.drawOffer = "threerep";
+      this.game = Object.assign(
+        // NOTE: assign mycolor here, since BaseGame could also be VS computer
+        {
+          type: gtype,
+          increment: tc.increment,
+          mycolor: mycolor,
+          // opponent sid not strictly required (or available), but easier
+          // at least oppsid or oppid is available anyway:
+          oppsid: myIdx < 0 ? undefined : game.players[1 - myIdx].sid,
+          oppid: myIdx < 0 ? undefined : game.players[1 - myIdx].id
+        },
+        game
+      );
+      this.$refs["basegame"].re_setVariables(this.game);
+      if (!this.gameIsLoading) {
+        // Initial loading:
+        this.gotMoveIdx = game.moves.length - 1;
+        // If we arrive here after 'nextGame' action, the board might be hidden
+        let boardDiv = document.querySelector(".game");
+        if (!!boardDiv && boardDiv.style.visibility == "hidden")
+          boardDiv.style.visibility = "visible";
+      }
+      this.re_setClocks();
+      this.$nextTick(() => {
+        this.game.rendered = true;
+        // Did lastate arrive before game was rendered?
+        if (this.lastate) this.processLastate();
+      });
+      if (this.lastateAsked) {
+        this.lastateAsked = false;
+        this.sendLastate(game.oppsid);
+      }
+      if (this.gameIsLoading) {
+        this.gameIsLoading = false;
+        if (this.gotMoveIdx >= game.moves.length)
+          // Some moves arrived meanwhile...
+          this.askGameAgain();
+      }
+      if (!!callback) callback();
+    },
+    fetchGame: function(game, callback) {
+      const afterRetrieval = async (game) => {
+        await import("@/variants/" + game.vname + ".js")
+        .then((vModule) => {
+          window.V = vModule[game.vname + "Rules"];
+          this.loadGame(game, callback);
         });
-        if (this.gameIsLoading) {
-          this.gameIsLoading = false;
-          if (this.gotMoveIdx >= game.moves.length)
-            // Some moves arrived meanwhile...
-            this.askGameAgain();
-        }
-        if (!!callback) callback();
       };
       if (!!game) {
         afterRetrieval(game);
         return;
       }
-      if (this.gameRef.rid) {
+      if (this.gameRef.rid)
         // Remote live game: forgetting about callback func... (TODO: design)
         this.send("askfullgame", { target: this.gameRef.rid });
-      else {
+      else {
         // Local or corr game on server.
         // NOTE: afterRetrieval() is never called if game not found
         const gid = this.gameRef.id;
@@ -933,11 +1079,10 @@ export default {
             {
               data: { gid: gid },
               success: (res) => {
-                let g = res.game;
-                g.moves.forEach(m => {
+                res.game.moves.forEach(m => {
                   m.squares = JSON.parse(m.squares);
                 });
-                afterRetrieval(g);
+                afterRetrieval(res.game);
               }
             }
           );
@@ -1015,15 +1160,13 @@ export default {
           const score = this.vr.getCurrentScore();
           if (score != "*") this.gameOver(score);
         }
-// TODO: notifyTurn: "changeturn" message
         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 {
+        } else {
+          // 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
@@ -1046,6 +1189,16 @@ export default {
           // NOTE: 'var' to see that variable outside this block
           var filtered_move = getFilteredMove(move);
         }
+        if (moveCol == this.game.mycolor && !data.receiveMyMove) {
+          // Notify turn on MyGames page:
+          this.notifyMyGames(
+            "turn",
+            {
+              gid: this.gameRef.id,
+              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 (
@@ -1071,7 +1224,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)
@@ -1123,13 +1275,8 @@ export default {
                 clearInterval(this.retrySendmove);
                 return;
               }
-              let oppsid = this.game.players[nextIdx].sid;
-              if (!oppsid) {
-                oppsid = Object.keys(this.people).find(
-                  sid => this.people[sid].id == this.game.players[nextIdx].uid
-                );
-              }
-              if (!oppsid || !this.people[oppsid])
+              const oppsid = this.getOppsid();
+              if (!oppsid)
                 // Opponent is disconnected: he'll ask last state
                 clearInterval(this.retrySendmove);
               else {
@@ -1183,6 +1330,8 @@ export default {
             position: position,
             orientation: V.CanFlip ? this.game.mycolor : "w"
           });
+          document.querySelector("#confirmDiv > .card").style.width =
+            boardDiv.offsetWidth + "px";
         } else {
           // Incomplete information: just ask confirmation
           // Hide the board, because otherwise it could reveal infos
@@ -1212,7 +1361,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
@@ -1227,6 +1376,14 @@ export default {
         else this.updateCorrGame(scoreObj, callback);
         // Notify the score to main Hall. TODO: only one player (currently double send)
         this.send("result", { gid: this.game.id, score: score });
+        // Also to MyGames page (TODO: doubled as well...)
+        this.notifyMyGames(
+          "score",
+          {
+            gid: this.gameRef.id,
+            score: score
+          }
+        );
       }
       else if (!!callback) callback();
     }
@@ -1235,6 +1392,10 @@ export default {
 </script>
 
 <style lang="sass" scoped>
+#infoDiv > .card
+  padding: 15px 0
+  max-width: 430px
+
 .connected
   background-color: lightgreen
 
@@ -1335,6 +1496,12 @@ span.yourturn
 .draw-threerep, .draw-threerep:hover
   background-color: #e4d1fc
 
+.rematch-sent, .rematch-sent:hover
+  background-color: lightyellow
+
+.rematch-received, .rematch-received:hover
+  background-color: lightgreen
+
 .somethingnew
   background-color: #c5fefe