Fix rematch process
[vchess.git] / client / src / views / Game.vue
index 282a371..024847c 100644 (file)
@@ -1,5 +1,13 @@
 <template lang="pug">
 main
+  input#modalInfo.modal(type="checkbox")
+  div#infoDiv(
+    role="dialog"
+    data-checkbox="modalInfo"
+  )
+    .card.text-center
+      label.modal-close(for="modalInfo")
+      p(v-html="infoMessage")
   input#modalChat.modal(
     type="checkbox"
     @click="resetChatColor()"
@@ -11,7 +19,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"
@@ -82,7 +90,8 @@ main
           img(src="/images/icons/resign.svg")
       button.tooltip(
         v-else-if="!!game.mycolor"
-        @click="rematch()"
+        @click="clickRematch()"
+        :class="{['rematch-' + rematchOffer]: true}"
         :aria-label="st.tr['Rematch']"
       )
         img(src="/images/icons/rematch.svg")
@@ -152,6 +161,9 @@ export default {
       virtualClocks: [],
       vr: null, //"variant rules" object initialized from FEN
       drawOffer: "",
+      infoMessage: "",
+      rematchOffer: "",
+      lastateAsked: false,
       people: {}, //players + observers
       onMygames: [], //opponents (or me) on "MyGames" page
       lastate: undefined, //used if opponent send lastate before game is ready
@@ -257,6 +269,8 @@ export default {
       this.virtualClocks = [[0,0], [0,0]];
       this.vr = null;
       this.drawOffer = "";
+      this.lastateAsked = false;
+      this.rematchOffer = "";
       this.onMygames = [];
       this.lastate = undefined;
       this.newChat = "";
@@ -350,6 +364,17 @@ export default {
         )
       );
     },
+    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;
+    },
     resetChatColor: function() {
       // TODO: this is called twice, once on opening an once on closing
       document.getElementById("chatBtn").classList.remove("somethingnew");
@@ -373,6 +398,9 @@ export default {
         this.$set(this.game, "chats", []);
       }
     },
+    getGameType: function(game) {
+      return game.cadence.indexOf("d") >= 0 ? "corr" : "live";
+    },
     // Notify turn after a new move (to opponent and me on MyGames page)
     notifyTurn: function(sid) {
       const player = this.people[sid];
@@ -393,10 +421,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;
@@ -560,7 +584,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) => {
@@ -577,29 +601,9 @@ export default {
           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
@@ -683,6 +687,47 @@ 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.uid == this.st.user.id)
+          ) {
+            this.$router.push("/game/" + gameInfo.id);
+          } else {
+            let urlRid = "";
+            if (gameInfo.cadence.indexOf('d') === -1) {
+              urlRid = "/?rid=";
+              // Select sid of any of the online players:
+              let onlineSid = [];
+              gameInfo.players.forEach(p => {
+                if (!!this.people[p.sid]) onlineSid.push(p.sid);
+              });
+              urlRid += onlineSid[Math.floor(Math.random() * onlineSid.length)];
+            }
+            this.infoMessage =
+              this.st.tr["Rematch in progress:"] +
+              " <a href='#/game/" +
+              gameInfo.id + urlRid +
+              "'>" +
+              "#/game/" +
+              gameInfo.id + urlRid +
+              "</a>";
+            document.getElementById("modalInfo").checked = true;
+          }
+          break;
+        }
         case "newchat":
           this.newChat = data.data;
           if (!document.getElementById("modalChat").checked)
@@ -710,6 +755,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,
+          score: 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 +793,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 +827,86 @@ 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(() => {});
+          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 = () => {
+          let oppsid = this.getOppsid(); //may be null
+          this.send("rnewgame", { data: gameInfo, oppsid: oppsid });
+        };
+        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");
@@ -772,11 +925,11 @@ 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 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 gtype = this.getGameType(game);
         const tc = extractTime(game.cadence);
         const myIdx = game.players.findIndex(p => {
           return p.sid == this.st.user.sid || p.uid == this.st.user.id;
@@ -845,6 +998,7 @@ export default {
             }
           }
         }
+        // TODO: merge next 2 "if" conditions
         if (!!game.drawOffer) {
           if (game.drawOffer == "t")
             // Three repetitions
@@ -863,6 +1017,18 @@ export default {
             }
           }
         }
+        if (!!game.rematchOffer) {
+          if (myIdx < 0) this.rematchOffer = "received";
+          else {
+            // I play in this game:
+            if (
+              (game.rematchOffer == "w" && myIdx == 0) ||
+              (game.rematchOffer == "b" && myIdx == 1)
+            )
+              this.rematchOffer = "sent";
+            else this.rematchOffer = "received";
+          }
+        }
         this.repeat = {}; //reset: scan past moves' FEN:
         let repIdx = 0;
         let vr_tmp = new V(game.fenStart);
@@ -906,6 +1072,10 @@ export default {
           // 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)
@@ -1123,13 +1293,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 {
@@ -1237,6 +1402,10 @@ export default {
 </script>
 
 <style lang="sass" scoped>
+#infoDiv > .card
+  padding: 15px 0
+  max-width: 430px
+
 .connected
   background-color: lightgreen
 
@@ -1337,6 +1506,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