Rollback last (bad) improving attempt
[vchess.git] / client / src / views / Game.vue
index fe48765..1071ece 100644 (file)
@@ -1,5 +1,14 @@
 <template lang="pug">
 main
+  input#modalRules.modal(type="checkbox")
+  div#rulesDiv(
+    role="dialog"
+    data-checkbox="modalRules"
+  )
+    .card
+      label.modal-close(for="modalRules")
+      a#variantNameInGame(:href="'/#/variants/'+game.vname") {{ game.vdisp }}
+      div(v-html="rulesContent")
   input#modalScore.modal(type="checkbox")
   div#scoreDiv(
     role="dialog"
@@ -37,7 +46,7 @@ main
         span {{ st.tr["Participant(s):"] }} 
         span(
           v-for="p in Object.values(people)"
-          v-if="participateInChat(p)"
+          v-if="!!p.name"
         )
           | {{ p.name }} 
         span.anonymous(v-if="someAnonymousPresent()") + @nonymous
@@ -67,49 +76,57 @@ main
         button.refuseBtn(@click="cancelMove()")
           span {{ st.tr["Cancel"] }}
   .row
-    #aboveBoard.col-sm-12.col-md-9.col-md-offset-3.col-lg-10.col-lg-offset-2
+    #aboveBoard.col-sm-12
       span.variant-cadence(v-if="game.type!='import'") {{ game.cadence }}
-      span.variant-name {{ game.vname }}
+      span.variant-name
+        | {{ game.vname }}
+        | {{ !!vr ? vr.constructor.AbbreviateOptions(game.options) : '' }}
       span#nextGame(
         v-if="nextIds.length > 0"
         @click="showNextGame()"
       )
         | {{ st.tr["Next_g"] }}
-      button#chatBtn.tooltip(
+      button#chatBtn(
+        :class="btnTooltipClass()"
         onClick="window.doClick('modalChat')"
         aria-label="Chat"
       )
         img(src="/images/icons/chat.svg")
       #actions(v-if="game.score=='*'")
-        button.tooltip(
+        button(
           @click="clickDraw()"
-          :class="{['draw-' + drawOffer]: true}"
+          :class="btnTooltipClass('draw')"
           :aria-label="st.tr['Draw']"
         )
           img(src="/images/icons/draw.svg")
-        button.tooltip(
+        button(
           v-if="!!game.mycolor"
+          :class="btnTooltipClass()"
           @click="abortGame()"
           :aria-label="st.tr['Abort']"
         )
           img(src="/images/icons/abort.svg")
-        button.tooltip(
+        button(
           v-if="!!game.mycolor"
+          :class="btnTooltipClass()"
           @click="resign()"
           :aria-label="st.tr['Resign']"
         )
           img(src="/images/icons/resign.svg")
-      button.tooltip(
+      button(
         v-else
+        :class="btnTooltipClass('rematch')"
         @click="clickRematch()"
-        :class="{['rematch-' + rematchOffer]: true}"
         :aria-label="st.tr['Rematch']"
       )
         img(src="/images/icons/rematch.svg")
       #playersInfo
-        p(v-if="isLargeScreen()")
-          span.name(:class="{connected: isConnected(0)}")
-            | {{ game.players[0].name || "@nonymous" }}
+        div(v-if="isLargeScreen()")
+          UserBio.user-bio(
+            :class="{connected: isConnected(0)}"
+            :uid="game.players[0].id"
+            :uname="game.players[0].name"
+          )
           span.time(
             v-if="game.score=='*'"
             :class="{yourturn: !!vr && vr.turn == 'w'}"
@@ -119,8 +136,11 @@ main
             span.time-right(v-if="!!virtualClocks[0][1]")
               | {{ virtualClocks[0][1] }}
           span.split-names -
-          span.name(:class="{connected: isConnected(1)}")
-            | {{ game.players[1].name || "@nonymous" }}
+          UserBio.user-bio(
+            :class="{connected: isConnected(1)}"
+            :uid="game.players[1].id"
+            :uname="game.players[1].name"
+          )
           span.time(
             v-if="game.score=='*'"
             :class="{yourturn: !!vr && vr.turn == 'b'}"
@@ -129,29 +149,30 @@ main
             span.time-separator(v-if="!!virtualClocks[1][1]") :
             span.time-right(v-if="!!virtualClocks[1][1]")
               | {{ virtualClocks[1][1] }}
-        p(v-else)
-          span.name(:class="{connected: isConnected(0)}")
-            | {{ game.players[0].name || "@nonymous" }}
-          span.split-names -
-          span.name(:class="{connected: isConnected(1)}")
-            | {{ game.players[1].name || "@nonymous" }}
-          br
-          span.time(
-            v-if="game.score=='*'"
-            :class="{yourturn: !!vr && vr.turn == 'w'}"
+        div(v-else)
+          UserBio.user-bio(
+            :class="{connected: isConnected(0)}"
+            :uid="game.players[0].id"
+            :uname="game.players[0].name"
           )
-            span.time-left {{ virtualClocks[0][0] }}
-            span.time-separator(v-if="!!virtualClocks[0][1]") :
-            span.time-right(v-if="!!virtualClocks[0][1]")
-              | {{ virtualClocks[0][1] }}
-          span.time(
-            v-if="game.score=='*'"
-            :class="{yourturn: !!vr && vr.turn == 'b'}"
+          span.split-names -
+          UserBio.user-bio(
+            :class="{connected: isConnected(1)}"
+            :uid="game.players[1].id"
+            :uname="game.players[1].name"
           )
-            span.time-left {{ virtualClocks[1][0] }}
-            span.time-separator(v-if="!!virtualClocks[1][1]") :
-            span.time-right(v-if="!!virtualClocks[1][1]")
-              | {{ virtualClocks[1][1] }}
+          div(v-if="game.score=='*'")
+            span.time(:class="{yourturn: !!vr && vr.turn == 'w'}")
+              span.time-left {{ virtualClocks[0][0] }}
+              span.time-separator(v-if="!!virtualClocks[0][1]") :
+              span.time-right(v-if="!!virtualClocks[0][1]")
+                | {{ virtualClocks[0][1] }}
+            span.separator
+            span.time(:class="{yourturn: !!vr && vr.turn == 'b'}")
+              span.time-left {{ virtualClocks[1][0] }}
+              span.time-separator(v-if="!!virtualClocks[1][1]") :
+              span.time-right(v-if="!!virtualClocks[1][1]")
+                | {{ virtualClocks[1][1] }}
   BaseGame(
     ref="basegame"
     :game="game"
@@ -161,6 +182,7 @@ main
 
 <script>
 import BaseGame from "@/components/BaseGame.vue";
+import UserBio from "@/components/UserBio.vue";
 import Chat from "@/components/Chat.vue";
 import { store } from "@/store";
 import { GameStorage } from "@/utils/gameStorage";
@@ -172,16 +194,18 @@ import { extractTime } from "@/utils/timeControl";
 import { getRandString } from "@/utils/alea";
 import { getScoreMessage } from "@/utils/scoring";
 import { getFullNotation } from "@/utils/notation";
-import { getDiagram } from "@/utils/printDiagram";
+import { getDiagram, replaceByDiag } from "@/utils/printDiagram";
 import { processModalClick } from "@/utils/modalClick";
 import { playMove, getFilteredMove } from "@/utils/playUndo";
 import { ArrayFun } from "@/utils/array";
+import afterRawLoad from "@/utils/afterRawLoad";
 import params from "@/parameters";
 export default {
   name: "my-game",
   components: {
     BaseGame,
-    Chat
+    Chat,
+    UserBio
   },
   data: function() {
     return {
@@ -192,8 +216,10 @@ export default {
       game: {}, //passed to BaseGame
       focus: !document.hidden, //will not always work... TODO
       // virtualClocks will be initialized from true game.clocks
+      // TODO: clock update triggers re-rendering. Should be out of Vue
       virtualClocks: [],
       vr: null, //"variant rules" object initialized from FEN
+      rulesContent: "",
       drawOffer: "",
       rematchId: "",
       rematchOffer: "",
@@ -204,9 +230,6 @@ export default {
       curDiag: "", //for corr moves confirmation
       conn: null,
       roomInitialized: false,
-      // If newmove has wrong index: ask fullgame again:
-      askGameTime: 0,
-      gameIsLoading: false,
       // If asklastate got no reply, ask again:
       gotLastate: false,
       gotMoveIdx: -1, //last move index received
@@ -237,7 +260,8 @@ export default {
           // In case of incomplete information variant:
           boardDiv.style.visibility = "hidden";
         this.atCreation();
-      } else
+      }
+      else
         // Same game ID
         this.nextIds = JSON.parse(this.$route.query["next"] || "[]");
     }
@@ -253,18 +277,12 @@ export default {
           this.toggleChat("close")
         });
       });
-    ["rematchDiv", "scoreDiv"].forEach(
+    ["rulesDiv", "rematchDiv", "scoreDiv"].forEach(
       (eltName) => {
         document.getElementById(eltName)
           .addEventListener("click", processModalClick);
       }
     );
-    if ("ontouchstart" in window) {
-      // Disable tooltips on smartphones:
-      document.querySelectorAll("#aboveBoard .tooltip").forEach(elt => {
-        elt.classList.remove("tooltip");
-      });
-    }
   },
   beforeDestroy: function() {
     this.cleanBeforeDestroy();
@@ -285,11 +303,6 @@ export default {
     visibilityChange: function() {
       // TODO: Use document.hidden? https://webplatform.news/issues/2019-03-27
       this.focus = (document.visibilityState == "visible");
-      if (!this.focus && !!this.rematchOffer) {
-        this.rematchOffer = "";
-        this.send("rematchoffer", { data: false });
-        // Do not remove rematch offer from (local) storage
-      }
       this.send(this.focus ? "getfocus" : "losefocus");
     },
     onFocus: function() {
@@ -298,17 +311,20 @@ export default {
     },
     onBlur: function() {
       this.focus = false;
-      if (!!this.rematchOffer) {
-        this.rematchOffer = "";
-        this.send("rematchoffer", { data: false });
-      }
       this.send("losefocus");
     },
     isLargeScreen: function() {
-      return window.innerWidth >= 500;
+      return window.innerWidth >= 768;
     },
-    participateInChat: function(p) {
-      return Object.keys(p.tmpIds).some(x => p.tmpIds[x].focus) && !!p.name;
+    btnTooltipClass: function(thing) {
+      let append = {};
+      if (!!thing) append = { [thing + "-" + this[thing + "Offer"]]: true };
+      return (
+        Object.assign(
+          { tooltip: !("ontouchstart" in window) },
+          append
+        )
+      );
     },
     someAnonymousPresent: function() {
       return (
@@ -317,6 +333,29 @@ export default {
         )
       );
     },
+    requestLastate: function(sid) {
+      // TODO: maybe also find opponent SID ?
+      //const oppSid =
+      //  this.game.players.find(p => p.sid != this.st.user.sid).sid;
+      this.send("asklastate", { target: sid });
+      let counter = 1;
+      this.askLastate = setInterval(
+        () => {
+          // Ask at most 3 times:
+          // if no reply after that there should be a network issue.
+          if (
+            counter < 3 &&
+            !this.gotLastate &&
+            !!this.people[sid]
+          ) {
+            this.send("asklastate", { target: sid });
+            counter++;
+          }
+          else clearInterval(this.askLastate);
+        },
+        1500
+      );
+    },
     atCreation: function() {
       document.addEventListener('visibilitychange', this.visibilityChange);
       window.addEventListener('focus', this.onFocus);
@@ -335,7 +374,7 @@ export default {
           id: my.id,
           name: my.name,
           tmpIds: {
-            tmpId: { focus: true }
+            [tmpId]: { focus: true }
           }
         }
       );
@@ -348,13 +387,12 @@ export default {
       if (!!chatComp) chatComp.chats = [];
       this.virtualClocks = [[0,0], [0,0]];
       this.vr = null;
+      this.rulesContent = "";
       this.drawOffer = "";
       this.lastateAsked = false;
       this.rematchOffer = "";
       this.lastate = undefined;
       this.roomInitialized = false;
-      this.askGameTime = 0;
-      this.gameIsLoading = false;
       this.gotLastate = false;
       this.gotMoveIdx = -1;
       this.opponentGotMove = false;
@@ -380,6 +418,8 @@ export default {
               "message", this.socketMessageListener);
             this.conn = new WebSocket(this.connexionString);
             this.conn.addEventListener("message", this.socketMessageListener);
+            const oppSid = this.getOppsid();
+            if (!!oppSid) this.requestLastate(oppSid); //in case of
           }
         },
         1000
@@ -395,8 +435,15 @@ export default {
           this.conn.onopen = () => callback();
       };
       this.fetchGame((game) => {
-        if (!!game)
+        if (!!game) {
+          if (!game.options) {
+            // Patch for retro-compatibility (TODO: remove it)
+            game.options = { randomness: game.randomness };
+            delete game["randomness"];
+          }
+          else game.options = JSON.parse(game.options);
           this.loadVariantThenGame(game, () => socketInit(this.roomInit));
+        }
         else
           // Live game stored remotely: need socket to retrieve it
           // NOTE: the callback "roomInit" will be lost, so it's not provided.
@@ -422,10 +469,14 @@ 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.id)
+      if (
+        this.st.user.sid == player.sid ||
+        (!!player.name && 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);
+      }
       // Try to find a match in people:
       return (
         (
@@ -439,7 +490,7 @@ export default {
         )
         ||
         (
-          !!player.id &&
+          player.id > 0 &&
           Object.values(this.people).some(p => {
             return (
               p.id == player.id &&
@@ -497,7 +548,8 @@ export default {
             "DELETE",
             { data: { gid: this.game.id } }
           );
-        } else {
+        }
+        else {
           // Live game
           GameStorage.update(this.gameRef, { delchat: true });
         }
@@ -506,7 +558,7 @@ export default {
     },
     getGameType: function(game) {
       if (!!game.id.toString().match(/^i/)) return "import";
-      return game.cadence.indexOf("d") >= 0 ? "corr" : "live";
+      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) {
@@ -528,27 +580,6 @@ export default {
       this.$router.push(
         "/game/" + nextGid + "/?next=" + JSON.stringify(this.nextIds));
     },
-    askGameAgain: function() {
-      this.gameIsLoading = true;
-      const currentUrl = document.location.href;
-      const doAskGame = () => {
-        if (document.location.href != currentUrl) return; //page change
-        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();
-      const delay = Math.max(2000 - (now - this.askGameTime), 0);
-      this.askGameTime = now;
-      setTimeout(doAskGame, delay);
-    },
     socketMessageListener: function(msg) {
       if (!this.conn) return;
       const data = JSON.parse(msg.data);
@@ -582,7 +613,8 @@ export default {
             // For self multi-connects tests:
             this.newConnect[data.from[0]] = true;
             this.send("askidentity", { target: data.from[0] });
-          } else {
+          }
+          else {
             this.people[data.from[0]].tmpIds[data.from[1]] = { focus: true };
             this.$forceUpdate(); //TODO: shouldn't be required
           }
@@ -663,28 +695,9 @@ export default {
             !this.gotLastate &&
             !!this.game.mycolor &&
             this.game.type == "live" &&
-            this.game.score == "*" &&
             this.game.players.some(p => p.sid == user.sid)
           ) {
-            this.send("asklastate", { target: user.sid });
-            let counter = 1;
-            this.askLastate = setInterval(
-              () => {
-                // Ask at most 3 times:
-                // if no reply after that there should be a network issue.
-                if (
-                  counter < 3 &&
-                  !this.gotLastate &&
-                  !!this.people[user.sid]
-                ) {
-                  this.send("asklastate", { target: user.sid });
-                  counter++;
-                } else {
-                  clearInterval(this.askLastate);
-                }
-              },
-              1500
-            );
+            this.requestLastate(user.sid);
           }
           break;
         }
@@ -711,7 +724,7 @@ export default {
           const gameToSend = Object.keys(this.game)
             .filter(k =>
               [
-                "id","fen","players","vid","cadence","fenStart","vname",
+                "id","fen","players","vid","cadence","fenStart","options",
                 "moves","clocks","score","drawOffer","rematchOffer"
               ].includes(k))
             .reduce(
@@ -739,10 +752,15 @@ export default {
           if (!this.game || !this.game.moves) this.lastateAsked = true;
           else this.sendLastate(data.from);
           break;
+        // TODO: possible bad scenario: reload page while oppponent sends a
+        // move => get both lastate and newmove, process both, add move twice.
+        // Confirm scenario? Fix?
         case "lastate": {
           // Got opponent infos about last move
           this.gotLastate = true;
           this.lastate = data.data;
+          if (this.lastate.movesCount - 1 > this.gotMoveIdx)
+            this.gotMoveIdx = this.lastate.movesCount - 1;
           if (this.game.rendered)
             // Game is rendered (Board component)
             this.processLastate();
@@ -750,64 +768,77 @@ export default {
           break;
         }
         case "newmove": {
+
+// DEBUG:
+//console.log("Receive move");
+//console.log(data.data);
+//moveslist not updated when receiving a move? (see in BaseGame)
+
           const movePlus = data.data;
           const movesCount = this.game.moves.length;
-          if (movePlus.index > movesCount) {
-            // This can only happen if I'm an observer and missed a move.
-            if (this.gotMoveIdx < movePlus.index)
-              this.gotMoveIdx = movePlus.index;
-            if (!this.gameIsLoading) this.askGameAgain();
+          if (
+            movePlus.index < movesCount ||
+            this.gotMoveIdx >= movePlus.index
+          ) {
+            // Opponent re-send but we already have the move:
+            // (maybe he didn't receive our pingback...)
+            this.send("gotmove", {data: movePlus.index, target: data.from});
           }
           else {
-            if (
-              movePlus.index < movesCount ||
-              this.gotMoveIdx >= movePlus.index
-            ) {
-              // Opponent re-send but we already have the move:
-              // (maybe he didn't receive our pingback...)
-              this.send("gotmove", {data: movePlus.index, target: data.from});
-            } else {
-              this.gotMoveIdx = movePlus.index;
-              const receiveMyMove = (movePlus.color == this.game.mycolor);
-              const moveColIdx = ["w", "b"].indexOf(movePlus.color);
-              if (!receiveMyMove && !!this.game.mycolor) {
-                // Notify opponent that I got the move:
-                this.send(
-                  "gotmove",
-                  { data: movePlus.index, target: data.from }
+            this.gotMoveIdx = movePlus.index;
+            const receiveMyMove = (movePlus.color == this.game.mycolor);
+            const moveColIdx = ["w", "b"].indexOf(movePlus.color);
+            if (!receiveMyMove && !!this.game.mycolor) {
+              // Notify opponent that I got the move:
+              this.send(
+                "gotmove",
+                { data: movePlus.index, target: data.from }
+              );
+              // And myself if I'm elsewhere:
+              if (!this.focus) {
+                notify(
+                  "New move",
+                  {
+                    body:
+                      (this.game.players[moveColIdx].name || "@nonymous") +
+                      " just played."
+                  }
                 );
-                // And myself if I'm elsewhere:
-                if (!this.focus) {
-                  notify(
-                    "New move",
-                    {
-                      body:
-                        (this.game.players[moveColIdx].name || "@nonymous") +
-                        " just played."
-                    }
-                  );
-                }
               }
-              if (movePlus.cancelDrawOffer) {
-                // Opponent refuses draw
-                this.drawOffer = "";
-                // NOTE for corr games: drawOffer reset by player in turn
-                if (
-                  this.game.type == "live" &&
-                  !!this.game.mycolor &&
-                  !receiveMyMove
-                ) {
-                  GameStorage.update(this.gameRef, { drawOffer: "" });
-                }
+            }
+            if (movePlus.cancelDrawOffer) {
+              // Opponent refuses draw
+              this.drawOffer = "";
+              // NOTE for corr games: drawOffer reset by player in turn
+              if (
+                this.game.type == "live" &&
+                !!this.game.mycolor &&
+                !receiveMyMove
+              ) {
+                GameStorage.update(this.gameRef, { drawOffer: "" });
               }
-              this.$refs["basegame"].play(
-                movePlus.move, "received", null, true);
-              this.game.clocks[moveColIdx] = movePlus.clock;
-              this.processMove(
-                movePlus.move,
-                { receiveMyMove: receiveMyMove }
-              );
             }
+            this.$refs["basegame"].play(movePlus.move, "received");
+            // Freeze time while the move is being play
+            // (TODO: a callback would be cleaner here)
+            clearInterval(this.clockUpdate);
+            this.clockUpdate = null;
+            const freezeDuration = ["all", "highlight"].includes(V.ShowMoves)
+              // 250 = length of animation, 500 = delay between sub-moves
+              ? 250 + 750 *
+                (Array.isArray(movePlus.move) ? movePlus.move.length - 1 : 0)
+              // Incomplete information: no move animation
+              : 0;
+            setTimeout(
+              () => {
+                this.game.clocks[moveColIdx] = movePlus.clock;
+                this.processMove(
+                  movePlus.move,
+                  { receiveMyMove: receiveMyMove }
+                );
+              },
+              freezeDuration
+            );
           }
           break;
         }
@@ -848,7 +879,7 @@ export default {
           if (!!this.game.mycolor && this.game.type == "live") {
             GameStorage.update(
               this.gameRef,
-              { rematchOffer: V.GetOppCol(this.game.mycolor) }
+              { rematchOffer: data.data ? V.GetOppCol(this.game.mycolor) : "" }
             );
           }
           break;
@@ -861,13 +892,18 @@ export default {
             gameInfo.players.some(p => p.sid == this.st.user.sid)
           ) {
             this.addAndGotoLiveGame(gameInfo);
-          } else if (
+          }
+          else if (
             gameType == "corr" &&
+            this.st.user.id > 0 &&
             gameInfo.players.some(p => p.id == this.st.user.id)
           ) {
             this.$router.push("/game/" + gameInfo.id);
-          } else {
+          }
+          else {
             this.rematchId = gameInfo.id;
+            document.getElementById("modalRules").checked = false;
+            document.getElementById("modalScore").checked = false;
             document.getElementById("modalRematch").checked = true;
           }
           break;
@@ -913,8 +949,8 @@ export default {
         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",
+        drawSent: this.drawOffer == "sent" ? true : undefined,
+        rematchSent: this.rematchOffer == "sent" ? true : undefined,
         score: this.game.score != "*" ? this.game.score : undefined,
         scoreMsg: this.game.score != "*" ? this.game.scoreMsg : undefined,
         movesCount: L
@@ -925,23 +961,48 @@ export default {
     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);
-      } else {
-        if (!!this.clockUpdate) clearInterval(this.clockUpdate);
-        this.re_setClocks();
-      }
-      if (data.drawSent) this.drawOffer = "received";
-      if (data.rematchSent) this.rematchOffer = "received";
       if (!!data.score) {
-        this.drawOffer = "";
-        if (this.game.score == "*")
-          this.gameOver(data.score, data.scoreMsg);
+        const oppCol = V.GetOppCol(this.game.mycolor);
+        if (!!data.rematchSent) {
+          if (this.game.rematchOffer != oppCol) {
+            // Opponent sended rematch offer while we were offline:
+            this.rematchOffer = "received";
+            GameStorage.update(
+              this.gameRef,
+              { rematchOffer: oppCol }
+            );
+          }
+        }
+        else {
+          if (this.game.rematchOffer == oppCol) {
+            // Opponent cancelled rematch offer while we were offline:
+            this.rematchOffer = "";
+            GameStorage.update(
+              this.gameRef,
+              { rematchOffer: "" }
+            );
+          }
+        }
+      }
+      else {
+        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");
+          this.processMove(data.lastMove);
+        }
+        else {
+          if (!!this.clockUpdate) clearInterval(this.clockUpdate);
+          this.re_setClocks();
+        }
+        if (!!data.drawSent) this.drawOffer = "received";
+        if (!!data.score) {
+          this.drawOffer = "";
+          if (this.game.score == "*")
+            this.gameOver(data.score, data.scoreMsg);
+        }
       }
     },
     clickDraw: function() {
@@ -954,7 +1015,8 @@ export default {
             : "Three repetitions";
         this.send("draw", { data: message });
         this.gameOver("1/2", message);
-      } else if (this.drawOffer == "") {
+      }
+      else if (this.drawOffer == "") {
         // No effect if drawOffer == "sent"
         if (this.game.mycolor != this.vr.turn) {
           alert(this.st.tr["Draw offer only in your turn"]);
@@ -968,7 +1030,8 @@ export default {
             this.gameRef,
             { drawOffer: this.game.mycolor }
           );
-        } else this.updateCorrGame({ drawOffer: this.game.mycolor });
+        }
+        else this.updateCorrGame({ drawOffer: this.game.mycolor });
       }
     },
     addAndGotoLiveGame: function(gameInfo, callback) {
@@ -978,11 +1041,11 @@ export default {
         {
           // (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
+          chats: [],
           score: "*"
         }
       );
@@ -1002,39 +1065,51 @@ export default {
         // Start a new game!
         let gameInfo = {
           id: getRandString(), //ignored if corr
-          fen: V.GenRandInitFen(this.game.randomness),
-          players: this.game.players.reverse(),
+          fen: V.GenRandInitFen(this.game.options),
+          options: JSON.stringify(this.game.options),
+          players: [this.game.players[1], this.game.players[0]],
           vid: this.game.vid,
           cadence: this.game.cadence
         };
         const notifyNewGame = () => {
-          const oppsid = this.getOppsid(); //may be null
-          this.send("rnewgame", { data: gameInfo, oppsid: oppsid });
+          this.send("rnewgame", { data: gameInfo });
           // To main Hall if corr game:
           if (this.game.type == "corr")
             this.send("newgame", { data: gameInfo, page: "/" });
           // Also to MyGames page:
           this.notifyMyGames("newgame", gameInfo);
         };
-        if (this.game.type == "live")
+        if (this.game.type == "live") {
+          GameStorage.update(
+            this.gameRef,
+            { rematchOffer: "" }
+          );
+          // Increment game stats counter in DB
+          ajax(
+            "/gamestat",
+            "POST",
+            { data: { vid: gameInfo.vid } }
+          );
           this.addAndGotoLiveGame(gameInfo, notifyNewGame);
+        }
         else {
           // corr game
+          this.updateCorrGame({ rematchOffer: 'n' });
           ajax(
             "/games",
             "POST",
             {
-              // cid is useful to delete the challenge:
               data: { gameInfo: gameInfo },
               success: (response) => {
-                gameInfo.id = response.gameId;
+                gameInfo.id = response.id;
                 notifyNewGame();
-                this.$router.push("/game/" + response.gameId);
+                this.$router.push("/game/" + response.id);
               }
             }
           );
         }
-      } else if (this.rematchOffer == "") {
+      }
+      else if (this.rematchOffer == "") {
         this.rematchOffer = "sent";
         this.send("rematchoffer", { data: true });
         if (this.game.type == "live") {
@@ -1042,8 +1117,10 @@ export default {
             this.gameRef,
             { rematchOffer: this.game.mycolor }
           );
-        } else this.updateCorrGame({ rematchOffer: this.game.mycolor });
-      } else if (this.rematchOffer == "sent") {
+        }
+        else this.updateCorrGame({ rematchOffer: this.game.mycolor });
+      }
+      else if (this.rematchOffer == "sent") {
         // Toggle rematch offer (on --> off)
         this.rematchOffer = "";
         this.send("rematchoffer", { data: false });
@@ -1052,7 +1129,8 @@ export default {
             this.gameRef,
             { rematchOffer: '' }
           );
-        } else this.updateCorrGame({ rematchOffer: 'n' });
+        }
+        else this.updateCorrGame({ rematchOffer: 'n' });
       }
     },
     abortGame: function() {
@@ -1073,7 +1151,10 @@ export default {
       const gtype = game.type || 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;
+        return (
+          p.sid == this.st.user.sid ||
+          (!!p.name && p.id == this.st.user.id)
+        );
       });
       // Sometimes the name isn't stored yet (TODO: why?)
       if (
@@ -1118,11 +1199,10 @@ export default {
               { clocks: game.clocks }
             );
           }
-        } else {
-          if (!!game.initime)
-            // It's my turn: clocks not updated yet
-            game.clocks[myIdx] -= (Date.now() - game.initime) / 1000;
         }
+        else if (!!game.initime)
+          // It's my turn: clocks not updated yet
+          game.clocks[myIdx] -= (Date.now() - game.initime) / 1000;
       }
       else
         // gtype == "import"
@@ -1165,8 +1245,9 @@ export default {
           if (
             (game.rematchOffer == "w" && myIdx == 0) ||
             (game.rematchOffer == "b" && myIdx == 1)
-          )
+          ) {
             this.rematchOffer = "sent";
+          }
           else this.rematchOffer = "received";
         }
       }
@@ -1198,45 +1279,64 @@ export default {
         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";
-      }
+      // 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.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);
-      });
+      const afterSetVname = async () => {
+        await import("@/variants/" + game.vname + ".js")
+        .then((vModule) => {
+          window.V = vModule[game.vname + "Rules"];
+          this.loadGame(game, callback);
+        });
+        this.rulesContent =
+          afterRawLoad(
+            require(
+              "raw-loader!@/translations/rules/" +
+              game.vname + "/" + this.st.lang + ".pug"
+            ).default
+          ).replace(/(fen:)([^:]*):/g, replaceByDiag);
+      };
+      let variant = undefined;
+      const trySetVname = setInterval(
+        () => {
+          // this.st.variants might be uninitialized (variant == null)
+          variant = this.st.variants.find(v => {
+            return v.id == game.vid || v.name == game.vname
+          });
+          if (!!variant) {
+            clearInterval(trySetVname);
+            game.vname = variant.name;
+            game.vdisp = variant.display;
+            afterSetVname();
+          }
+        }, 500
+      );
     },
     // 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))) {
+      if (
+        Number.isInteger(this.gameRef) ||
+        !isNaN(parseInt(this.gameRef, 10))
+      ) {
         // corr games identifiers are integers
         ajax(
           "/games",
@@ -1282,7 +1382,8 @@ export default {
                 currentTurn == "w" ? "0-1" : "1-0",
                 "Time"
               );
-          } else {
+          }
+          else {
             this.$set(
               this.virtualClocks,
               colorIdx,
@@ -1338,14 +1439,20 @@ export default {
           // In corr games, just reset clock to mainTime:
           this.game.clocks[colorIdx] = extractTime(this.game.cadence).mainTime;
         }
-        // If repetition detected, consider that a draw offer was received:
-        const fenObj = this.vr.getFenForRepeat();
-        this.repeat[fenObj] =
-          !!this.repeat[fenObj]
-            ? this.repeat[fenObj] + 1
-            : 1;
-        if (this.repeat[fenObj] >= 3) this.drawOffer = "threerep";
-        else if (this.drawOffer == "threerep") this.drawOffer = "";
+        if (!V.IgnoreRepetition) {
+          // If repetition detected, consider that a draw offer was received:
+          const fenObj = this.vr.getFenForRepeat();
+          this.repeat[fenObj] =
+            !!this.repeat[fenObj]
+              ? this.repeat[fenObj] + 1
+              : 1;
+          if (this.repeat[fenObj] >= 3) {
+            if (this.vr.loseOnRepetition())
+              this.gameOver(moveCol == "w" ? "0-1" : "1-0", "Repetition");
+            else this.drawOffer = "threerep";
+          }
+          else if (this.drawOffer == "threerep") this.drawOffer = "";
+        }
         if (!!this.game.mycolor && !data.receiveMyMove) {
           // NOTE: 'var' to see that variable outside this block
           var filtered_move = getFilteredMove(move);
@@ -1436,7 +1543,7 @@ export default {
           this.opponentGotMove = false;
           this.send("newmove", {data: sendMove});
           // If the opponent doesn't reply gotmove soon enough, re-send move:
-          // Do this at most 2 times, because mpore would mean network issues,
+          // Do this at most 2 times, because more would mean network issues,
           // opponent would then be expected to disconnect/reconnect.
           let counter = 1;
           const currentUrl = document.location.href;
@@ -1483,6 +1590,10 @@ export default {
             if (data.score == "*") this.re_setClocks();
           }
         };
+        if (!V.CorrConfirm) {
+          afterSetScore();
+          return;
+        }
         let el = document.querySelector("#buttonsConfirm > .acceptBtn");
         // We may play several moves in a row: in case of, remove listener:
         let elClone = el.cloneNode(true);
@@ -1507,11 +1618,14 @@ export default {
         if (["all","byrow"].includes(V.ShowMoves)) {
           this.curDiag = getDiagram({
             position: position,
-            orientation: V.CanFlip ? this.game.mycolor : "w"
+            orientation: V.CanFlip ? this.game.mycolor : "w",
+            color: this.game.mycolor,
+            score: "*"
           });
           document.querySelector("#confirmDiv > .card").style.width =
             boardDiv.offsetWidth + "px";
-        } else {
+        }
+        else {
           // Incomplete information: just ask confirmation
           // Hide the board, because otherwise it could reveal infos
           boardDiv.style.visibility = "hidden";
@@ -1536,13 +1650,17 @@ export default {
     // In corr games, callback to change page only after score is set:
     gameOver: function(score, scoreMsg, callback) {
       this.game.score = score;
-      if (!scoreMsg) scoreMsg = getScoreMessage(score);
+      if (!scoreMsg) scoreMsg = getScoreMessage(score, V.ReverseColors);
       this.game.scoreMsg = scoreMsg;
+      document.getElementById("modalRules").checked = false;
       // Display result in a un-missable way:
       document.getElementById("modalScore").checked = true;
       this.$set(this.game, "scoreMsg", scoreMsg);
       const myIdx = this.game.players.findIndex(p => {
-        return p.sid == this.st.user.sid || p.id == this.st.user.id;
+        return (
+          p.sid == this.st.user.sid ||
+          (!!p.name && p.id == this.st.user.id)
+        );
       });
       if (myIdx >= 0) {
         // OK, I play in this game
@@ -1582,10 +1700,22 @@ export default {
 
 <style lang="sass" scoped>
 #scoreDiv > .card, #rematchDiv > .card
-  padding: 15px 0
+  padding: 10px 0
   max-width: 430px
 
+#rulesDiv > .card
+  padding: 5px 0
+  max-width: 50%
+  max-height: 100%
+  @media screen and (max-width: 1500px)
+    max-width: 67%
+  @media screen and (max-width: 1024px)
+    max-width: 85%
+  @media screen and (max-width: 767px)
+    max-width: 100%
+
 p.score-section
+  margin: 0
   font-size: 1.3em
   span.score
     font-weight: bold
@@ -1603,9 +1733,6 @@ p.score-section
 #playersInfo > p
   margin: 0
 
-@media screen and (min-width: 768px)
-  #actions
-    width: 300px
 @media screen and (max-width: 767px)
   .game
     width: 100%
@@ -1624,12 +1751,15 @@ button
     @media screen and (max-width: 767px)
       height: 18px
 
-@media screen and (max-width: 767px)
-  #aboveBoard
-    text-align: center
-@media screen and (min-width: 768px)
-  #aboveBoard
-    margin-left: 30%
+#aboveBoard
+  text-align: center
+
+.user-bio
+  display: inline
+  font-size: 1.5rem
+  @media screen and (max-width: 767px)
+    font-size: 1.2rem
+  padding: 0 3px
 
 .variant-cadence
   padding-right: 10px
@@ -1644,12 +1774,16 @@ span#nextGame
   display: inline-block
   margin-right: 10px
 
-span.name
-  font-size: 1.5rem
-  padding: 0 3px
+span.separator
+  display: inline-block
+  margin: 0
+  padding: 0
+  width: 10px
 
 span.time
   font-size: 2rem
+  @media screen and (max-width: 767px)
+    font-size: 1.5rem
   display: inline-block
   .time-left
     margin-left: 10px
@@ -1685,19 +1819,19 @@ span.yourturn
   background-color: lightyellow
 
 .draw-received, .draw-received:hover
-  background-color: lightgreen
+  background-color: #73C6B6
 
 .draw-threerep, .draw-threerep:hover
-  background-color: #e4d1fc
+  background-color: #D2B4DE
 
 .rematch-sent, .rematch-sent:hover
   background-color: lightyellow
 
 .rematch-received, .rematch-received:hover
-  background-color: lightgreen
+  background-color: #48C9B0
 
 .somethingnew
-  background-color: #c5fefe
+  background-color: #D2B4DE
 
 .diagram
   margin: 0 auto
@@ -1713,4 +1847,17 @@ button.acceptBtn
   background-color: lightgreen
 button.refuseBtn
   background-color: red
+
+a#variantNameInGame
+  color: var(--card-fore-color)
+  text-align: center
+  font-weight: bold
+  font-size: calc(1rem * var(--heading-ratio))
+  line-height: 1.2
+  margin: calc(1.5 * var(--universal-margin))
+</style>
+
+<style lang="sass">
+@import "@/styles/_rules.sass"
+@import "@/styles/_board_squares_img.sass"
 </style>