Fix anonymous games which started with same color
[vchess.git] / client / src / views / Game.vue
index 398a0a2..e0eb1be 100644 (file)
@@ -1,15 +1,35 @@
 <template lang="pug">
 main
-  input#modalInfo.modal(type="checkbox")
-  div#infoDiv(
+  input#modalRules.modal(type="checkbox")
+  div#rulesDiv(
     role="dialog"
-    data-checkbox="modalInfo"
+    data-checkbox="modalRules"
+  )
+    .card
+      label.modal-close(for="modalRules")
+      h4#variantNameInGame(@click="gotoRules") {{ game.vname }}
+      div(v-html="rulesContent")
+  input#modalScore.modal(type="checkbox")
+  div#scoreDiv(
+    role="dialog"
+    data-checkbox="modalScore"
+  )
+    .card.text-center
+      label.modal-close(for="modalScore")
+      p.score-section
+        span.score {{ game.score }}
+        | &nbsp;:&nbsp;
+        span.score-msg {{ st.tr[game.scoreMsg] }}
+  input#modalRematch.modal(type="checkbox")
+  div#rematchDiv(
+    role="dialog"
+    data-checkbox="modalRematch"
   )
     .card.text-center
-      label.modal-close(for="modalInfo")
+      label.modal-close(for="modalRematch")
       a(
         :href="'#/game/' + rematchId"
-        onClick="document.getElementById('modalInfo').checked=false"
+        onClick="document.getElementById('modalRematch').checked=false"
       )
         | {{ st.tr["Rematch in progress"] }}
   input#modalChat.modal(
@@ -56,7 +76,7 @@ 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#nextGame(
@@ -64,39 +84,42 @@ main
         @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()")
+        div(v-if="isLargeScreen()")
           span.name(:class="{connected: isConnected(0)}")
             | {{ game.players[0].name || "@nonymous" }}
           span.time(
@@ -118,29 +141,24 @@ main
             span.time-separator(v-if="!!virtualClocks[1][1]") :
             span.time-right(v-if="!!virtualClocks[1][1]")
               | {{ virtualClocks[1][1] }}
-        p(v-else)
+        div(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'}"
-          )
-            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.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,7 +179,7 @@ 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";
@@ -183,6 +201,7 @@ export default {
       // virtualClocks will be initialized from true game.clocks
       virtualClocks: [],
       vr: null, //"variant rules" object initialized from FEN
+      rulesContent: "",
       drawOffer: "",
       rematchId: "",
       rematchOffer: "",
@@ -242,14 +261,12 @@ export default {
           this.toggleChat("close")
         });
       });
-    document.getElementById("infoDiv")
-      .addEventListener("click", processModalClick);
-    if ("ontouchstart" in window) {
-      // Disable tooltips on smartphones:
-      document.querySelectorAll("#aboveBoard .tooltip").forEach(elt => {
-        elt.classList.remove("tooltip");
-      });
-    }
+    ["rulesDiv", "rematchDiv", "scoreDiv"].forEach(
+      (eltName) => {
+        document.getElementById(eltName)
+          .addEventListener("click", processModalClick);
+      }
+    );
   },
   beforeDestroy: function() {
     this.cleanBeforeDestroy();
@@ -270,11 +287,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() {
@@ -283,14 +295,23 @@ 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;
+    },
+    btnTooltipClass: function(thing) {
+      let append = {};
+      if (!!thing) append = { [thing + "-" + this[thing + "Offer"]]: true };
+      return (
+        Object.assign(
+          { tooltip: !("ontouchstart" in window) },
+          append
+        )
+      );
+    },
+    gotoRules: function() {
+      this.$router.push("/variants/" + this.game.vname);
     },
     participateInChat: function(p) {
       return Object.keys(p.tmpIds).some(x => p.tmpIds[x].focus) && !!p.name;
@@ -333,6 +354,7 @@ export default {
       if (!!chatComp) chatComp.chats = [];
       this.virtualClocks = [[0,0], [0,0]];
       this.vr = null;
+      this.rulesContent = "";
       this.drawOffer = "";
       this.lastateAsked = false;
       this.rematchOffer = "";
@@ -407,10 +429,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 (
         (
@@ -424,7 +450,7 @@ export default {
         )
         ||
         (
-          !!player.id &&
+          player.id > 0 &&
           Object.values(this.people).some(p => {
             return (
               p.id == player.id &&
@@ -545,7 +571,8 @@ export default {
             if (sid != this.st.user.sid) {
               this.send("askidentity", { target: sid });
               this.people[sid] = { tmpIds: data.sockIds[sid] };
-            } else {
+            }
+            else {
               // Complete my tmpIds:
               Object.assign(this.people[sid].tmpIds, data.sockIds[sid]);
             }
@@ -611,6 +638,23 @@ export default {
           // player.tmpIds is already set
           player.name = user.name;
           player.id = user.id;
+          if (this.game.type == "live") {
+            const myGidx =
+              this.game.players.findIndex(p => p.sid == this.st.user.sid);
+            // Sometimes a player name isn't stored yet (TODO: why?)
+            if (
+              myGidx >= 0 &&
+              !this.game.players[1 - myGidx].name &&
+              this.game.players[1 - myGidx].sid == user.sid &&
+              !!user.name
+            ) {
+              this.game.players[1-myGidx].name = user.name;
+              GameStorage.update(
+                this.gameRef,
+                { playerName: { idx: 1 - myGidx, name: user.name } }
+              );
+            }
+          }
           this.$forceUpdate(); //TODO: shouldn't be required
           // If I multi-connect, kill current connexion if no mark (I'm older)
           if (this.newConnect[user.sid]) {
@@ -630,7 +674,6 @@ 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 });
@@ -767,12 +810,26 @@ export default {
                   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
               );
             }
           }
@@ -815,7 +872,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;
@@ -830,12 +887,15 @@ export default {
             this.addAndGotoLiveGame(gameInfo);
           } 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 {
             this.rematchId = gameInfo.id;
-            document.getElementById("modalInfo").checked = true;
+            document.getElementById("modalRules").checked = false;
+            document.getElementById("modalScore").checked = false;
+            document.getElementById("modalRematch").checked = true;
           }
           break;
         }
@@ -880,8 +940,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
@@ -892,23 +952,47 @@ 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() {
@@ -1040,8 +1124,24 @@ 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 (
+        myIdx >= 0 &&
+        gtype == "live" &&
+        !game.players[myIdx].name &&
+        !!this.st.user.name
+      ) {
+        game.players[myIdx].name = this.st.user.name;
+        GameStorage.update(
+          game.id,
+          { playerName: { idx: myIdx, name: this.st.user.name } }
+        );
+      }
       // "mycolor" is undefined for observers
       const mycolor = [undefined, "w", "b"][myIdx + 1];
       if (gtype == "corr") {
@@ -1067,9 +1167,10 @@ export default {
           game.clocks = [tc.mainTime, tc.mainTime];
           if (myIdx >= 0) {
             // I play in this live game
-            GameStorage.update(game.id, {
-              clocks: game.clocks
-            });
+            GameStorage.update(
+              game.id,
+              { clocks: game.clocks }
+            );
           }
         } else {
           if (!!game.initime)
@@ -1118,8 +1219,9 @@ export default {
           if (
             (game.rematchOffer == "w" && myIdx == 0) ||
             (game.rematchOffer == "b" && myIdx == 1)
-          )
+          ) {
             this.rematchOffer = "sent";
+          }
           else this.rematchOffer = "received";
         }
       }
@@ -1183,6 +1285,19 @@ export default {
         window.V = vModule[game.vname + "Rules"];
         this.loadGame(game, callback);
       });
+      // (AJAX) Request to get rules content (plain text, HTML)
+      this.rulesContent =
+        require(
+          "raw-loader!@/translations/rules/" +
+          game.vname + "/" +
+          this.st.lang + ".pug"
+        )
+        // Next two lines fix a weird issue after last update (2019-11)
+        .replace(/\\n/g, " ")
+        .replace(/\\"/g, '"')
+        .replace('module.exports = "', "")
+        .replace(/"$/, "")
+        .replace(/(fen:)([^:]*):/g, replaceByDiag);
     },
     // 3 cases for loading a game:
     //  - from indexedDB (running or completed live game I play)
@@ -1491,9 +1606,15 @@ export default {
       this.game.score = score;
       if (!scoreMsg) scoreMsg = getScoreMessage(score);
       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
@@ -1532,10 +1653,27 @@ export default {
 </script>
 
 <style lang="sass" scoped>
-#infoDiv > .card
-  padding: 15px 0
+#scoreDiv > .card, #rematchDiv > .card
+  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
+
 .connected
   background-color: lightgreen
 
@@ -1549,9 +1687,6 @@ export default {
 #playersInfo > p
   margin: 0
 
-@media screen and (min-width: 768px)
-  #actions
-    width: 300px
 @media screen and (max-width: 767px)
   .game
     width: 100%
@@ -1570,12 +1705,8 @@ 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
 
 .variant-cadence
   padding-right: 10px
@@ -1590,12 +1721,22 @@ span#nextGame
   display: inline-block
   margin-right: 10px
 
+span.separator
+  display: inline-block
+  margin: 0
+  padding: 0
+  width: 10px
+
 span.name
   font-size: 1.5rem
+  @media screen and (max-width: 767px)
+    font-size: 1.2rem
   padding: 0 3px
 
 span.time
   font-size: 2rem
+  @media screen and (max-width: 767px)
+    font-size: 1.5rem
   display: inline-block
   .time-left
     margin-left: 10px
@@ -1659,4 +1800,15 @@ button.acceptBtn
   background-color: lightgreen
 button.refuseBtn
   background-color: red
+
+h4#variantNameInGame
+  cursor: pointer
+  text-align: center
+  text-decoration: underline
+  font-weight: bold
+</style>
+
+<style lang="sass">
+@import "@/styles/_rules.sass"
+@import "@/styles/_board_squares_img.sass"
 </style>