Draft Ball variant + some fixes, enhancements and code cleaning
[vchess.git] / client / src / views / Hall.vue
index b9262d1..9bb913c 100644 (file)
@@ -73,7 +73,10 @@ main
           )
         fieldset(v-if="st.user.id > 0")
           label(for="selectPlayers") {{ st.tr["Play with"] }}
-          select#selectPlayersInList(v-model="newchallenge.to")
+          select#selectPlayersInList(
+            v-model="newchallenge.to"
+            @change="changeChallTarget()"
+          )
             option(value="")
             option(
               v-for="p in Object.values(people)"
@@ -120,10 +123,10 @@ main
               @click="challenge(sid)"
             )
               | {{ st.tr["Challenge"] }}
-          p.anonymous @nonymous ({{ anonymousCount }})
+          p.anonymous @nonymous ({{ anonymousCount() }})
         #chat
           Chat(
-            :newChat="newChat"
+            ref="chatcomp"
             @mychat="processChat"
             :pastChats="[]"
           )
@@ -131,7 +134,7 @@ main
   .row
     .col-sm-12.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
       .button-group
-        button#peopleBtn(onClick="window.doClick('modalPeople')")
+        button#peopleBtn(@click="openModalPeople()")
           | {{ st.tr["Who's there?"] }}
         button(@click="showNewchallengeForm()")
           | {{ st.tr["New game"] }}
@@ -193,7 +196,7 @@ main
           )
           button#loadMoreBtn(
             v-if="hasMore"
-            @click="loadMore()"
+            @click="loadMoreCorr()"
           )
             | {{ st.tr["Load more"] }}
 </template>
@@ -247,7 +250,6 @@ export default {
       tchallDiag: "",
       curChallToAccept: {from: {}},
       presetChalls: JSON.parse(localStorage.getItem("presetChalls") || "[]"),
-      newChat: "",
       conn: null,
       connexionString: "",
       // Related to (killing of) self multi-connects:
@@ -266,16 +268,6 @@ export default {
         this.loadNewchallVariant();
     }
   },
-  computed: {
-    anonymousCount: function() {
-      let count = 0;
-      Object.values(this.people).forEach(p => {
-        // Do not cound people who did not send their identity yet:
-        count += (!p.name && p.id === 0) ? 1 : 0;
-      });
-      return count;
-    }
-  },
   created: function() {
     if (this.st.variants.length > 0 && this.newchallenge.vid > 0)
       this.loadNewchallVariant();
@@ -307,8 +299,8 @@ export default {
       encodeURIComponent(this.$route.path);
     this.conn = new WebSocket(this.connexionString);
     this.conn.onopen = connectAndPoll;
-    this.conn.onmessage = this.socketMessageListener;
-    this.conn.onclose = this.socketCloseListener;
+    this.conn.addEventListener("message", this.socketMessageListener);
+    this.conn.addEventListener("close", this.socketCloseListener);
   },
   mounted: function() {
     document.addEventListener('visibilitychange', this.visibilityChange);
@@ -329,41 +321,9 @@ export default {
     this.setDisplay('c', showCtype);
     this.setDisplay('g', showGtype);
     // Ask server for current corr games (all but mines)
-    ajax(
-      "/observedgames",
-      "GET",
-      {
-        data: {
-          uid: this.st.user.id,
-          cursor: this.cursor
-        },
-        success: (response) => {
-          if (
-            response.games.length > 0 &&
-            this.games.length == 0 &&
-            this.gdisplay == "live"
-          ) {
-            document
-              .getElementById("btnGcorr")
-              .classList.add("somethingnew");
-          }
-          this.games = this.games.concat(
-            response.games.map(g => {
-              const vname = this.getVname(g.vid);
-              return Object.assign(
-                {},
-                g,
-                {
-                  type: "corr",
-                  vname: vname
-                }
-              );
-            })
-          );
-        }
-      }
-    );
+    this.loadMoreCorr();
     // Also ask for corr challenges (open + sent by/to me)
+    // List them all, because they are not supposed to be that many (TODO?)
     ajax(
       "/challenges",
       "GET",
@@ -436,6 +396,18 @@ export default {
         ["random-" + pc.randomness]: true
       };
     },
+    openModalPeople: function() {
+      window.doClick("modalPeople");
+      document.getElementById("inputChat").focus();
+    },
+    anonymousCount: function() {
+      let count = 0;
+      Object.values(this.people).forEach(p => {
+        // Do not cound people who did not send their identity yet:
+        count += (!p.name && p.id === 0) ? 1 : 0;
+      });
+      return count;
+    },
     visibilityChange: function() {
       // TODO: Use document.hidden? https://webplatform.news/issues/2019-03-27
       this.send(
@@ -484,6 +456,13 @@ export default {
       if (!!this.curChallToAccept.fen) return { "margin-top": "10px" };
       return {};
     },
+    changeChallTarget: function() {
+      if (!this.newchallenge.to) {
+        // Reset potential FEN + diagram
+        this.newchallenge.fen = "";
+        this.newchallenge.diag = "";
+      }
+    },
     cadenceFocusIfOpened: function() {
       if (event.target.checked)
         document.getElementById("cadence").focus();
@@ -560,10 +539,7 @@ export default {
     showGame: function(g) {
       // NOTE: we are an observer, since only games I don't play are shown here
       // ==> Moves sent by connected remote player(s) if live game
-      let url = "/game/" + g.id;
-      if (g.type == "live")
-        url += "?rid=" + g.rids[Math.floor(Math.random() * g.rids.length)];
-      this.$router.push(url);
+      this.$router.push("/game/" + g.id);
     },
     resetSocialColor: function() {
       // TODO: this is called twice, once on opening an once on closing
@@ -590,6 +566,8 @@ export default {
           // Since people can be both in Hall and Game,
           // need to track "askIdentity" requests:
           let identityAsked = {};
+          // TODO: shuffling and random filtering on server, if
+          // the room is really crowded.
           data.sockIds.forEach(s => {
             const page = s.page || "/";
             if (s.sid != this.st.user.sid && !identityAsked[s.sid]) {
@@ -605,21 +583,26 @@ export default {
             if (!s.page)
               // Peer is in Hall
               this.send("askchallenges", { target: s.sid });
-            // Peer is in Game
-            else this.send("askgame", { target: s.sid, page: page });
+            // Peer is in Game: ask only if live game
+            else if (!page.match(/\/[0-9]+$/))
+              this.send("askgame", { target: s.sid, page: page });
           });
           break;
         }
         case "connect":
         case "gconnect": {
           const page = data.page || "/";
-          // Only ask game / challenges if first connexion:
-          if (!this.people[data.from]) {
-            this.people[data.from] = { pages: [{ path: page, focus: true }] };
-            if (data.code == "connect")
+          if (data.code == "connect") {
+            // Ask challenges only on first connexion:
+            if (!this.people[data.from])
               this.send("askchallenges", { target: data.from });
-            else this.send("askgame", { target: data.from, page: page });
-          } else {
+          }
+          // Ask game only if live:
+          else if (!page.match(/\/[0-9]+$/))
+            this.send("askgame", { target: data.from, page: page });
+          if (!this.people[data.from])
+            this.people[data.from] = { pages: [{ path: page, focus: true }] };
+          else {
             // Append page if not already in list
             if (!(this.people[data.from].pages.find(p => p.path == page)))
               this.people[data.from].pages.push({ path: page, focus: true });
@@ -637,6 +620,10 @@ export default {
           // the first reload won't have time to connect but will trigger a "close" event anyway.
           // ==> Next check is required.
           if (!this.people[data.from]) return;
+          const page = data.page || "/";
+          ArrayFun.remove(this.people[data.from].pages, p => p.path == page);
+          if (this.people[data.from].pages.length == 0)
+            this.$delete(this.people, data.from);
           // Disconnect means no more tmpIds:
           if (data.code == "disconnect") {
             // Remove the live challenges sent by this player:
@@ -647,22 +634,16 @@ export default {
             );
           } else {
             // Remove the matching live game if now unreachable
-            const gid = data.page.match(/[a-zA-Z0-9]+$/)[0];
+            const gid = page.match(/[a-zA-Z0-9]+$/)[0];
             // Corr games are always reachable:
             if (!gid.match(/^[0-9]+$/)) {
-              const gidx = this.games.findIndex(g => g.id == gid);
-              // NOTE: gidx should always be >= 0 (TODO?)
-              if (gidx >= 0) {
-                const game = this.games[gidx];
-                ArrayFun.remove(game.rids, rid => rid == data.from);
-                if (game.rids.length == 0) this.games.splice(gidx, 1);
+              // Live games are reachable as long as someone is on the game page
+              if (Object.values(this.people).every(p =>
+                p.pages.every(pg => pg.path != page))) {
+                ArrayFun.remove(this.games, g => g.id == gid);
               }
             }
           }
-          const page = data.page || "/";
-          ArrayFun.remove(this.people[data.from].pages, p => p.path == page);
-          if (this.people[data.from].pages.length == 0)
-            this.$delete(this.people, data.from);
           break;
         }
         case "getfocus":
@@ -682,6 +663,8 @@ export default {
           break;
         case "killed":
           // I logged in elsewhere:
+          this.conn.removeEventListener("message", this.socketMessageListener);
+          this.conn.removeEventListener("close", this.socketCloseListener);
           this.conn = null;
           alert(this.st.tr["New connexion detected: tab now offline"]);
           break;
@@ -772,35 +755,30 @@ export default {
           }
           break;
         }
-        case "game": {
-          // Individual request
+        case "game": // Individual request
+        case "newgame": {
           const game = data.data;
-          // Ignore games where I play (will go in MyGames page)
-          if (game.players.every(p =>
-            p.sid != this.st.user.sid && p.id != this.st.user.id))
-          {
-            let locGame = this.games.find(g => g.id == game.id);
-            if (!locGame) {
-              let newGame = game;
-              newGame.type = this.classifyObject(game);
-              newGame.vname = this.getVname(game.vid);
-              if (!game.score)
-                // New game from Hall
-                newGame.score = "*";
-              newGame.rids = [game.rid];
-              delete newGame["rid"];
-              this.games.push(newGame);
-              if (
-                (newGame.type == "live" && this.gdisplay == "corr") ||
-                (newGame.type == "corr" && this.gdisplay == "live")
-              ) {
-                document
-                  .getElementById("btnG" + newGame.type)
-                  .classList.add("somethingnew");
-              }
-            } else {
-              // Append rid (if not already in list)
-              if (!locGame.rids.includes(game.rid)) locGame.rids.push(game.rid);
+          // Ignore games where I play (will go in MyGames page),
+          // and also games that I already received.
+          if (
+            game.players.every(p =>
+              p.sid != this.st.user.sid && p.id != this.st.user.id) &&
+            this.games.findIndex(g => g.id == game.id) == -1
+          ) {
+            let newGame = game;
+            newGame.type = this.classifyObject(game);
+            newGame.vname = this.getVname(game.vid);
+            if (!game.score)
+              // New game from Hall
+              newGame.score = "*";
+            this.games.push(newGame);
+            if (
+              (newGame.type == "live" && this.gdisplay == "corr") ||
+              (newGame.type == "corr" && this.gdisplay == "live")
+            ) {
+              document
+                .getElementById("btnG" + newGame.type)
+                .classList.add("somethingnew");
             }
           }
           break;
@@ -829,7 +807,7 @@ export default {
           break;
         }
         case "newchat":
-          this.newChat = data.data;
+          this.$refs["chatcomp"].newChat(data.data);
           if (!document.getElementById("modalPeople").checked)
             document.getElementById("peopleBtn").classList.add("somethingnew");
           break;
@@ -841,7 +819,7 @@ export default {
       this.conn.addEventListener("message", this.socketMessageListener);
       this.conn.addEventListener("close", this.socketCloseListener);
     },
-    loadMore: function() {
+    loadMoreCorr: function() {
       ajax(
         "/observedgames",
         "GET",
@@ -851,8 +829,18 @@ export default {
             cursor: this.cursor
           },
           success: (res) => {
-            if (res.games.length > 0) {
-              const L = res.games.length;
+            const L = res.games.length;
+            if (L > 0) {
+              if (
+                this.cursor == Number.MAX_SAFE_INTEGER &&
+                this.games.length == 0 &&
+                this.gdisplay == "live"
+              ) {
+                // First loading: show indicators
+                document
+                  .getElementById("btnGcorr")
+                  .classList.add("somethingnew");
+              }
               this.cursor = res.games[L - 1].created;
               let moreGames = res.games.map(g => {
                 const vname = this.getVname(g.vid);
@@ -900,10 +888,13 @@ export default {
     },
     loadNewchallVariant: async function(cb) {
       const vname = this.getVname(this.newchallenge.vid);
-      const vModule = await import("@/variants/" + vname + ".js");
-      this.newchallenge.V = vModule.VariantRules;
-      this.newchallenge.vname = vname;
-      if (!!cb) cb();
+      await import("@/variants/" + vname + ".js")
+      .then((vModule) => {
+        window.V = vModule[vname + "Rules"];
+        this.newchallenge.V = window.V;
+        this.newchallenge.vname = vname;
+        if (!!cb) cb();
+      });
     },
     trySetNewchallDiag: function() {
       if (!this.newchallenge.fen) {
@@ -922,7 +913,7 @@ export default {
           position: parsedFen.position,
           orientation: parsedFen.turn
         });
-      }
+      } else this.newchallenge.diag = "";
     },
     newChallFromPreset(pchall) {
       this.partialResetNewchallenge();
@@ -1015,7 +1006,6 @@ export default {
         });
         // Add new challenge:
         chall.from = {
-          // Decompose to avoid revealing email
           sid: this.st.user.sid,
           id: this.st.user.id,
           name: this.st.user.name
@@ -1049,7 +1039,7 @@ export default {
           {
             data: { chall: chall },
             success: (response) => {
-              finishAddChallenge(response.cid);
+              finishAddChallenge(response.id);
             }
           }
         );
@@ -1065,7 +1055,6 @@ export default {
     finishProcessingChallenge: function(c) {
       if (c.accepted) {
         c.seat = {
-          // Again, avoid c.seat = st.user to not reveal email
           sid: this.st.user.sid,
           id: this.st.user.id,
           name: this.st.user.name
@@ -1102,22 +1091,24 @@ export default {
           return;
         }
         c.accepted = true;
-        const vModule = await import("@/variants/" + c.vname + ".js");
-        window.V = vModule.VariantRules;
-        if (!!c.to) {
-          // c.to == this.st.user.name (connected)
-          if (!!c.fen) {
-            const parsedFen = V.ParseFen(c.fen);
-            c.mycolor = V.GetOppCol(parsedFen.turn);
-            this.tchallDiag = getDiagram({
-              position: parsedFen.position,
-              orientation: c.mycolor
-            });
+        await import("@/variants/" + c.vname + ".js")
+        .then((vModule) => {
+          window.V = vModule[c.vname + "Rules"];
+          if (!!c.to) {
+            // c.to == this.st.user.name (connected)
+            if (!!c.fen) {
+              const parsedFen = V.ParseFen(c.fen);
+              c.mycolor = V.GetOppCol(parsedFen.turn);
+              this.tchallDiag = getDiagram({
+                position: parsedFen.position,
+                orientation: c.mycolor
+              });
+            }
+            this.curChallToAccept = c;
+            document.getElementById("modalAccept").checked = true;
           }
-          this.curChallToAccept = c;
-          document.getElementById("modalAccept").checked = true;
-        }
-        else this.finishProcessingChallenge(c);
+          else this.finishProcessingChallenge(c);
+        });
       }
       else {
         // My challenge
@@ -1135,7 +1126,8 @@ export default {
     },
     // NOTE: when launching game, the challenge is already being deleted
     launchGame: function(c) {
-      let players =
+      // White player index 0, black player index 1:
+      const players =
         !!c.mycolor
           ? (c.mycolor == "w" ? [c.seat, c.from] : [c.from, c.seat])
           : shuffle([c.from, c.seat]);
@@ -1144,10 +1136,7 @@ export default {
         id: getRandString(),
         fen: c.fen || V.GenRandInitFen(c.randomness),
         randomness: c.randomness, //for rematch
-        // White player index 0, black player index 1:
-        players: c.mycolor
-          ? (c.mycolor == "w" ? [c.seat, c.from] : [c.from, c.seat])
-          : shuffle([c.from, c.seat]),
+        players: players,
         vid: c.vid,
         cadence: c.cadence
       };
@@ -1156,14 +1145,22 @@ export default {
         if (!!oppsid)
           // Opponent is online
           this.send("startgame", { data: gameInfo, target: oppsid });
+        // If new corr game, notify Hall (except opponent and me)
+        if (c.type == "corr") {
+          this.send(
+            "newgame",
+            {
+              data: gameInfo,
+              excluded: [this.st.user.sid, oppsid]
+            }
+          );
+        }
         // Notify MyGames page:
         this.send(
           "notifynewgame",
           {
             data: gameInfo,
-            targets: gameInfo.players.map(p => {
-              return { sid: p.sid, id: p.id };
-            })
+            targets: gameInfo.players
           }
         );
         // NOTE: no need to send the game to the room, since I'll connect
@@ -1179,11 +1176,14 @@ export default {
           "POST",
           {
             // cid is useful to delete the challenge:
-            data: { gameInfo: gameInfo, cid: c.id },
+            data: {
+              gameInfo: gameInfo,
+              cid: c.id
+            },
             success: (response) => {
-              gameInfo.id = response.gameId;
+              gameInfo.id = response.id;
               notifyNewgame();
-              this.$router.push("/game/" + response.gameId);
+              this.$router.push("/game/" + response.id);
             }
           }
         );
@@ -1202,7 +1202,6 @@ export default {
           // Game state (including FEN): will be updated
           moves: [],
           clocks: [-1, -1], //-1 = unstarted
-          initime: [0, 0], //initialized later
           score: "*"
         }
       );
@@ -1328,8 +1327,8 @@ tr > td
     margin: 5px 0
 
 button#loadMoreBtn
-  margin-top: 0
-  margin-bottom: 0
+  display: block
+  margin: 0 auto
 
 td.remove-preset
   background-color: lightgrey