From f14572c4a22425033735253eabbaa2d8dbb53d05 Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 16 Mar 2020 19:52:22 +0100
Subject: [PATCH] Refactor Games structure on server: no longer use an extra
 'Players' table

---
 client/public/images/pieces/SOURCE |   1 +
 client/src/App.vue                 |   2 +
 client/src/components/BaseGame.vue |   2 +-
 client/src/components/GameList.vue |   4 +-
 client/src/translations/en.js      |   2 +-
 client/src/translations/es.js      |   2 +-
 client/src/translations/fr.js      |   2 +-
 client/src/variants/Eightpieces.js |  26 +++-
 client/src/views/Game.vue          |  49 +++----
 client/src/views/Hall.vue          |  68 +++++-----
 client/src/views/MyGames.vue       |  14 +-
 server/models/Game.js              | 202 +++++++++++++++++------------
 server/routes/challenges.js        |   2 +-
 server/routes/games.js             |  19 +--
 server/sockets.js                  |  33 ++---
 15 files changed, 242 insertions(+), 186 deletions(-)

diff --git a/client/public/images/pieces/SOURCE b/client/public/images/pieces/SOURCE
index 26cb11c2..ec69ae2e 100644
--- a/client/public/images/pieces/SOURCE
+++ b/client/public/images/pieces/SOURCE
@@ -2,3 +2,4 @@ SVG standard images found on chesstempo website:
 https://www4.chesstempo.com/images/pieces/svg/merida/whitepawn.vers1.svg (...)
 + Adaptation for checkered pieces
 Some fairy pieces found on the web and icon scout: https://iconscout.com/
+PNG images for Eightpieces from https://greenchess.net/index.php and Jeff Kubach design.
diff --git a/client/src/App.vue b/client/src/App.vue
index 6a998080..6590fcfb 100644
--- a/client/src/App.vue
+++ b/client/src/App.vue
@@ -274,7 +274,9 @@ footer
 
 @media screen and (max-width: 420px)
   footer
+    height: 55px
     display: block
+    padding: 5px 0
 
 .menuitem.somenews
   color: red
diff --git a/client/src/components/BaseGame.vue b/client/src/components/BaseGame.vue
index 471c139c..612ded48 100644
--- a/client/src/components/BaseGame.vue
+++ b/client/src/components/BaseGame.vue
@@ -309,6 +309,7 @@ export default {
       const playSubmove = (smove) => {
         if (!navigate) smove.notation = this.vr.getNotation(smove);
         this.vr.play(smove);
+        this.lastMove = smove;
         if (!navigate) {
           if (!this.inMultimove) {
             if (this.cursor < this.moves.length - 1)
@@ -356,7 +357,6 @@ export default {
             smove.fen = this.vr.getFen();
           // Is opponent in check?
           this.incheck = this.vr.getCheckSquares(this.vr.turn);
-          this.lastMove = smove;
           this.emitFenIfAnalyze();
           this.inMultimove = false;
           if (!noemit) {
diff --git a/client/src/components/GameList.vue b/client/src/components/GameList.vue
index 549e2a1d..a83eb85a 100644
--- a/client/src/components/GameList.vue
+++ b/client/src/components/GameList.vue
@@ -85,7 +85,9 @@ export default {
       const deltaCreated = maxCreated - minCreated;
       return remGames.sort((g1, g2) => {
         return (
-          g2.priority - g1.priority + (g2.created - g1.created) / deltaCreated
+          g2.priority - g1.priority +
+          // Modulate with creation time (value in ]0,1[)
+          (g2.created - g1.created) / (deltaCreated + 1)
         );
       });
     },
diff --git a/client/src/translations/en.js b/client/src/translations/en.js
index a37a75c1..5a14d3f7 100644
--- a/client/src/translations/en.js
+++ b/client/src/translations/en.js
@@ -109,7 +109,7 @@ export const translations = {
   Register: "Register",
   "Registration complete! Please check your emails now": "Registration complete! Please check your emails now",
   Rematch: "Rematch",
-  "Rematch in progress:": "Rematch in progress",
+  "Rematch in progress": "Rematch in progress",
   "Remove game?": "Remove game?",
   Resign: "Resign",
   "Resign the game?": "Resign the game?",
diff --git a/client/src/translations/es.js b/client/src/translations/es.js
index e1048b7d..34444758 100644
--- a/client/src/translations/es.js
+++ b/client/src/translations/es.js
@@ -109,7 +109,7 @@ export const translations = {
   Register: "Registrarse",
   "Registration complete! Please check your emails now": "¡Registro completo! Revise sus correos electrónicos ahora",
   Rematch: "Revancha",
-  "Rematch in progress:": "Revancha en progreso:",
+  "Rematch in progress": "Revancha en progreso",
   "Remove game?": "¿Eliminar la partida?",
   Resign: "Abandonar",
   "Resign the game?": "¿Abandonar la partida?",
diff --git a/client/src/translations/fr.js b/client/src/translations/fr.js
index 20b7b587..8c1f7ae1 100644
--- a/client/src/translations/fr.js
+++ b/client/src/translations/fr.js
@@ -109,7 +109,7 @@ export const translations = {
   Register: "S'enregistrer",
   "Registration complete! Please check your emails now": "Enregistrement terminé ! Allez voir vos emails maintenant",
   Rematch: "Rejouer",
-  "Rematch in progress:": "Revanche en cours :",
+  "Rematch in progress": "Revanche en cours",
   "Remove game?": "Supprimer la partie ?",
   Resign: "Abandonner",
   "Resign the game?": "Abandonner la partie ?",
diff --git a/client/src/variants/Eightpieces.js b/client/src/variants/Eightpieces.js
index f7d93d84..76f72b0f 100644
--- a/client/src/variants/Eightpieces.js
+++ b/client/src/variants/Eightpieces.js
@@ -933,7 +933,7 @@ export const VariantRules = class EightpiecesRules extends ChessRules {
         return false;
       }
       let sq = [ x1 + step[0], y1 + step[1] ];
-      while (sq[0] != x2 && sq[1] != y2) {
+      while (sq[0] != x2 || sq[1] != y2) {
         if (
           // NOTE: no need to check OnBoard in this special case
           (!lancer && this.board[sq[0]][sq[1]] != V.EMPTY) ||
@@ -1080,9 +1080,24 @@ export const VariantRules = class EightpiecesRules extends ChessRules {
     return (!choice.second ? choice : [choice, choice.second]);
   }
 
+  // For moves notation:
+  static get LANCER_DIRNAMES() {
+    return {
+      'c': "N",
+      'd': "NE",
+      'e': "E",
+      'f': "SE",
+      'g': "S",
+      'h': "SW",
+      'm': "W",
+      'o': "NW"
+    };
+  }
+
   getNotation(move) {
     // Special case "king takes jailer" is a pass move
     if (move.appear.length == 0 && move.vanish.length == 0) return "pass";
+    let notation = undefined;
     if (this.subTurn == 2) {
       // Do not consider appear[1] (sentry) for sentry pushes
       const simpleMove = {
@@ -1091,8 +1106,11 @@ export const VariantRules = class EightpiecesRules extends ChessRules {
         start: move.start,
         end: move.end
       };
-      return super.getNotation(simpleMove);
-    }
-    return super.getNotation(move);
+      notation = super.getNotation(simpleMove);
+    } else notation = super.getNotation(move);
+    if (Object.keys(V.LANCER_DIRNAMES).includes(move.vanish[0].p))
+      // Lancer: add direction info
+      notation += "=" + V.LANCER_DIRNAMES[move.appear[0].p];
+    return notation;
   }
 };
diff --git a/client/src/views/Game.vue b/client/src/views/Game.vue
index a401e464..0182352a 100644
--- a/client/src/views/Game.vue
+++ b/client/src/views/Game.vue
@@ -7,13 +7,11 @@ main
   )
     .card.text-center
       label.modal-close(for="modalInfo")
-      p
-        span {{ st.tr["Rematch in progress:"] }}
-        a(
-          :href="'#/game/' + rematchId"
-          onClick="document.getElementById('modalInfo').checked=false"
-        )
-          | {{ "#/game/" + rematchId }}
+      a(
+        :href="'#/game/' + rematchId"
+        onClick="document.getElementById('modalInfo').checked=false"
+      )
+        | {{ st.tr["Rematch in progress"] }}
   input#modalChat.modal(
     type="checkbox"
     @click="resetChatColor()"
@@ -365,7 +363,7 @@ export default {
         )
         ||
         (
-          player.id &&
+          !!player.id &&
           Object.values(this.people).some(p =>
             p.id == player.id && p.focus)
         )
@@ -849,8 +847,11 @@ export default {
           cadence: this.game.cadence
         };
         const notifyNewGame = () => {
-          let oppsid = this.getOppsid(); //may be null
+          const oppsid = this.getOppsid(); //may be null
           this.send("rnewgame", { data: gameInfo, oppsid: oppsid });
+          // To main Hall if corr game:
+          if (this.game.type == "corr")
+            this.send("newgame", { data: gameInfo });
           // Also to MyGames page:
           this.notifyMyGames("newgame", gameInfo);
         };
@@ -923,20 +924,13 @@ export default {
         const mycolor = [undefined, "w", "b"][myIdx + 1]; //undefined for observers
         if (!game.chats) game.chats = []; //live games don't have chat history
         if (gtype == "corr") {
-          if (game.players[0].color == "b") {
-            // Adopt the same convention for live and corr games: [0] = white
-            [game.players[0], game.players[1]] = [
-              game.players[1],
-              game.players[0]
-            ];
-          }
           // NOTE: clocks in seconds, initime in milliseconds
           game.moves.sort((m1, m2) => m1.idx - m2.idx); //in case of
           game.clocks = [tc.mainTime, tc.mainTime];
           const L = game.moves.length;
           if (game.score == "*") {
             // Set clocks + initime
-            game.initime = [0, 0];
+            game.initime = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
             if (L >= 1) game.initime[L % 2] = game.moves[L-1].played;
             // NOTE: game.clocks shouldn't be computed right now:
             // job will be done in re_setClocks() called soon below.
@@ -966,17 +960,15 @@ export default {
           game.moves = game.moves.map(m => m.squares);
         }
         if (gtype == "live" && game.clocks[0] < 0) {
-          // Game is unstarted
+          // Game is unstarted. clocks and initime are ignored until move 2
           game.clocks = [tc.mainTime, tc.mainTime];
-          if (game.score == "*") {
-            game.initime[0] = Date.now();
-            if (myIdx >= 0) {
-              // I play in this live game; corr games don't have clocks+initime
-              GameStorage.update(game.id, {
-                clocks: game.clocks,
-                initime: game.initime
-              });
-            }
+          game.initime = [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER];
+          if (myIdx >= 0) {
+            // I play in this live game
+            GameStorage.update(game.id, {
+              clocks: game.clocks,
+              initime: game.initime
+            });
           }
         }
         // TODO: merge next 2 "if" conditions
@@ -1085,9 +1077,6 @@ export default {
                 g.moves.forEach(m => {
                   m.squares = JSON.parse(m.squares);
                 });
-                g.players = [{ id: g.white }, { id: g.black }];
-                delete g["white"];
-                delete g["black"];
                 afterRetrieval(g);
               }
             }
diff --git a/client/src/views/Hall.vue b/client/src/views/Hall.vue
index b9262d12..37558a93 100644
--- a/client/src/views/Hall.vue
+++ b/client/src/views/Hall.vue
@@ -338,14 +338,14 @@ export default {
           cursor: this.cursor
         },
         success: (response) => {
-          if (
-            response.games.length > 0 &&
-            this.games.length == 0 &&
-            this.gdisplay == "live"
-          ) {
-            document
-              .getElementById("btnGcorr")
-              .classList.add("somethingnew");
+          const L = response.games.length;
+          if (L > 0) {
+            this.cursor = response.games[L - 1].created;
+            if (this.games.length == 0 && this.gdisplay == "live") {
+              document
+                .getElementById("btnGcorr")
+                .classList.add("somethingnew");
+            }
           }
           this.games = this.games.concat(
             response.games.map(g => {
@@ -605,8 +605,9 @@ 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;
         }
@@ -618,7 +619,9 @@ export default {
             this.people[data.from] = { pages: [{ path: page, focus: true }] };
             if (data.code == "connect")
               this.send("askchallenges", { target: data.from });
-            else this.send("askgame", { target: data.from, page: page });
+            // Ask game only if live:
+            else if (!page.match(/\/[0-9]+$/))
+              this.send("askgame", { target: data.from, page: page });
           } else {
             // Append page if not already in list
             if (!(this.people[data.from].pages.find(p => p.path == page)))
@@ -772,8 +775,8 @@ 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 =>
@@ -1015,7 +1018,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 +1051,7 @@ export default {
           {
             data: { chall: chall },
             success: (response) => {
-              finishAddChallenge(response.cid);
+              finishAddChallenge(response.id);
             }
           }
         );
@@ -1065,7 +1067,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
@@ -1135,7 +1136,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 +1146,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 +1155,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 +1186,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);
             }
           }
         );
@@ -1328,8 +1338,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
diff --git a/client/src/views/MyGames.vue b/client/src/views/MyGames.vue
index 628d72eb..1f1da59e 100644
--- a/client/src/views/MyGames.vue
+++ b/client/src/views/MyGames.vue
@@ -103,16 +103,21 @@ export default {
           "/runninggames",
           "GET",
           {
+            credentials: true,
             success: (res) => {
               // These games are garanteed to not be deleted
               this.corrGames = res.games;
-              this.corrGames.forEach(g => g.type = "corr");
+              this.corrGames.forEach(g => {
+                g.type = "corr";
+                g.score = "*";
+              });
               this.decorate(this.corrGames);
               // Now ask completed games (partial list)
               ajax(
                 "/completedgames",
                 "GET",
                 {
+                  credentials: true,
                   data: { cursor: this.cursor },
                   success: (res2) => {
                     if (res2.games.length > 0) {
@@ -191,7 +196,7 @@ export default {
           if (thing == "turn") {
             game.myTurn = !game.myTurn;
             if (game.myTurn) this.tryShowNewsIndicator(type);
-          }
+          } else game.myTurn = false;
           // TODO: forcing refresh like that is ugly and wrong.
           //       How to do it cleanly?
           this.$refs[type + "games"].$forceUpdate();
@@ -287,6 +292,7 @@ export default {
         "/completedgames",
         "GET",
         {
+          credentials: true,
           data: { cursor: this.cursor },
           success: (res) => {
             if (res.games.length > 0) {
@@ -316,8 +322,8 @@ table.game-list
   max-height: 100%
 
 button#loadMoreBtn
-  margin-top: 0
-  margin-bottom: 0
+  display: block
+  margin: 0 auto
 
 .somethingnew
   background-color: #c5fefe !important
diff --git a/server/models/Game.js b/server/models/Game.js
index 65aedfde..080cab3c 100644
--- a/server/models/Game.js
+++ b/server/models/Game.js
@@ -38,10 +38,10 @@ const GameModel =
     return (
       g.vid.toString().match(/^[0-9]+$/) &&
       g.cadence.match(/^[0-9dhms +]+$/) &&
-      g.randomness.match(/^[0-2]$/) &&
+      g.randomness.toString().match(/^[0-2]$/) &&
       g.fen.match(/^[a-zA-Z0-9, /-]*$/) &&
       g.players.length == 2 &&
-      g.players.every(p => p.toString().match(/^[0-9]+$/))
+      g.players.every(p => p.id.toString().match(/^[0-9]+$/))
     );
   },
 
@@ -57,11 +57,11 @@ const GameModel =
         "VALUES " +
         "(" +
           vid + ",'" + fen + "','" + fen + "'," + randomness + "," +
-          "'" + players[0] + "','" + players[1] + "," +
+          players[0].id + "," + players[1].id + "," +
           "'" + cadence + "'," + Date.now() +
         ")";
       db.run(query, function(err) {
-        cb(err, { id: this.lastId });
+        cb(err, { id: this.lastID });
       });
     });
   },
@@ -72,7 +72,7 @@ const GameModel =
     db.serialize(function() {
       let query =
         "SELECT " +
-          "g.id, g.vid, g.fen, g.fenStart, g.cadence, g.created, " +
+          "g.id, g.fen, g.fenStart, g.cadence, g.created, " +
           "g.white, g.black, g.score, g.scoreMsg, " +
           "g.drawOffer, g.rematchOffer, v.name AS vname " +
         "FROM Games g " +
@@ -80,25 +80,40 @@ const GameModel =
         "  ON g.vid = v.id " +
         "WHERE g.id = " + id;
       db.get(query, (err, gameInfo) => {
+        if (!gameInfo) {
+          cb(err || { errmsg: "Game not found" }, undefined);
+          return;
+        }
         query =
-          "SELECT squares, played, idx " +
-          "FROM Moves " +
-          "WHERE gid = " + id;
-        db.all(query, (err3, moves) => {
+          "SELECT id, name " +
+          "FROM Users " +
+          "WHERE id IN (" + gameInfo.white + "," + gameInfo.black + ")";
+        db.all(query, (err2, players) => {
+          if (players[0].id == gameInfo.black) players = players.reverse();
+          // The original players' IDs info isn't required anymore
+          delete gameInfo["white"];
+          delete gameInfo["black"];
           query =
-            "SELECT msg, name, added " +
-            "FROM Chats " +
+            "SELECT squares, played, idx " +
+            "FROM Moves " +
             "WHERE gid = " + id;
-          db.all(query, (err4, chats) => {
-            const game = Object.assign(
-              {},
-              gameInfo,
-              {
-                moves: moves,
-                chats: chats
-              }
-            );
-            cb(null, game);
+          db.all(query, (err3, moves) => {
+            query =
+              "SELECT msg, name, added " +
+              "FROM Chats " +
+              "WHERE gid = " + id;
+            db.all(query, (err4, chats) => {
+              const game = Object.assign(
+                {},
+                gameInfo,
+                {
+                  players: players,
+                  moves: moves,
+                  chats: chats
+                }
+              );
+              cb(null, game);
+            });
           });
         });
       });
@@ -109,9 +124,8 @@ const GameModel =
   getObserved: function(uid, cursor, cb) {
     db.serialize(function() {
       let query =
-        "SELECT g.id, g.vid, g.cadence, g.created, " +
-        "       g.score, g.white, g.black " +
-        "FROM Games g ";
+        "SELECT id, vid, cadence, created, score, white, black " +
+        "FROM Games ";
       if (uid > 0) query +=
         "WHERE " +
         "  created < " + cursor + " AND " +
@@ -131,16 +145,19 @@ const GameModel =
           let names = {};
           users.forEach(u => { names[u.id] = u.name; });
           cb(
+            err,
             games.map(
               g => {
                 return {
                   id: g.id,
+                  vid: g.vid,
                   cadence: g.cadence,
-                  vname: g.vname,
                   created: g.created,
                   score: g.score,
-                  white: names[g.white],
-                  black: names[g.black]
+                  players: [
+                    { id: g.white, name: names[g.white] },
+                    { id: g.black, name: names[g.black] }
+                  ]
                 };
               }
             )
@@ -154,12 +171,12 @@ const GameModel =
   getRunning: function(uid, cb) {
     db.serialize(function() {
       let query =
-        "SELECT g.id, g.cadence, g.created, g.score, " +
+        "SELECT g.id, g.cadence, g.created, " +
           "g.white, g.black, v.name AS vname " +
         "FROM Games g " +
         "JOIN Variants v " +
         "  ON g.vid = v.id " +
-        "WHERE white = " + uid + " OR black = " + uid;
+        "WHERE score = '*' AND (white = " + uid + " OR black = " + uid + ")";
       db.all(query, (err, games) => {
         // Get movesCount (could be done in // with next query)
         query =
@@ -176,21 +193,24 @@ const GameModel =
             if (!pids[g.white]) pids[g.white] = true;
             if (!pids[g.black]) pids[g.black] = true;
           });
-          UserModel.getByIds(pids, (err2, users) => {
+          UserModel.getByIds(Object.keys(pids), (err2, users) => {
             let names = {};
             users.forEach(u => { names[u.id] = u.name; });
             cb(
+              null,
               games.map(
                 g => {
                   return {
                     id: g.id,
-                    cadence: g.cadence,
                     vname: g.vname,
+                    cadence: g.cadence,
                     created: g.created,
                     score: g.score,
-                    movesCount: movesCounts[g.id],
-                    white: names[g.white],
-                    black: names[g.black]
+                    movesCount: movesCounts[g.id] || 0,
+                    players: [
+                      { id: g.white, name: names[g.white] },
+                      { id: g.black, name: names[g.black] }
+                    ]
                   };
                 }
               )
@@ -212,6 +232,7 @@ const GameModel =
         "JOIN Variants v " +
         "  ON g.vid = v.id " +
         "WHERE " +
+        "  score <> '*' AND " +
         "  created < " + cursor + " AND " +
         "  (" +
         "    (" + uid + " = white AND NOT deletedByWhite) OR " +
@@ -227,21 +248,24 @@ const GameModel =
           if (!pids[g.white]) pids[g.white] = true;
           if (!pids[g.black]) pids[g.black] = true;
         });
-        UserModel.getByIds(pids, (err2, users) => {
+        UserModel.getByIds(Object.keys(pids), (err2, users) => {
           let names = {};
           users.forEach(u => { names[u.id] = u.name; });
           cb(
+            null,
             games.map(
               g => {
                 return {
                   id: g.id,
-                  cadence: g.cadence,
                   vname: g.vname,
+                  cadence: g.cadence,
                   created: g.created,
                   score: g.score,
                   scoreMsg: g.scoreMsg,
-                  white: names[g.white],
-                  black: names[g.black],
+                  players: [
+                    { id: g.white, name: names[g.white] },
+                    { id: g.black, name: names[g.black] }
+                  ],
                   deletedByWhite: g.deletedByWhite,
                   deletedByBlack: g.deletedByBlack
                 };
@@ -258,8 +282,8 @@ const GameModel =
       const query =
         "SELECT white, black " +
         "FROM Games " +
-        "WHERE gid = " + id;
-      db.all(query, (err, players) => {
+        "WHERE id = " + id;
+      db.get(query, (err, players) => {
         return cb(err, players);
       });
     });
@@ -292,71 +316,83 @@ const GameModel =
   // obj can have fields move, chat, fen, drawOffer and/or score + message
   update: function(id, obj, cb) {
     db.parallelize(function() {
-      let query =
+      let updateQuery =
         "UPDATE Games " +
         "SET ";
       let modifs = "";
       // NOTE: if drawOffer is set, we should check that it's player's turn
       // A bit overcomplicated. Let's trust the client on that for now...
-      if (!!obj.drawOffer)
-      {
-        if (obj.drawOffer == "n") //special "None" update
+      if (!!obj.drawOffer) {
+        if (obj.drawOffer == "n")
+          // Special "None" update
           obj.drawOffer = "";
         modifs += "drawOffer = '" + obj.drawOffer + "',";
       }
-      if (!!obj.rematchOffer)
-      {
-        if (obj.rematchOffer == "n") //special "None" update
+      if (!!obj.rematchOffer) {
+        if (obj.rematchOffer == "n")
+          // Special "None" update
           obj.rematchOffer = "";
         modifs += "rematchOffer = '" + obj.rematchOffer + "',";
       }
-      if (!!obj.fen)
-        modifs += "fen = '" + obj.fen + "',";
-      if (!!obj.score)
-        modifs += "score = '" + obj.score + "',";
-      if (!!obj.scoreMsg)
-        modifs += "scoreMsg = '" + obj.scoreMsg + "',";
+      if (!!obj.fen) modifs += "fen = '" + obj.fen + "',";
       if (!!obj.deletedBy) {
         const myColor = obj.deletedBy == 'w' ? "White" : "Black";
         modifs += "deletedBy" + myColor + " = true,";
       }
-      modifs = modifs.slice(0,-1); //remove last comma
-      if (modifs.length > 0)
-      {
-        query += modifs + " WHERE id = " + id;
-        db.run(query);
+      if (!!obj.score) {
+        modifs += "score = '" + obj.score + "'," +
+                  "scoreMsg = '" + obj.scoreMsg + "',";
       }
-      // NOTE: move, chat and delchat are mutually exclusive
-      if (!!obj.move)
-      {
-        // Security: only update moves if index is right
-        query =
-          "SELECT MAX(idx) AS maxIdx " +
+      const finishAndSendQuery = () => {
+        modifs = modifs.slice(0, -1); //remove last comma
+        if (modifs.length > 0) {
+          updateQuery += modifs + " WHERE id = " + id;
+          db.run(updateQuery);
+        }
+        cb(null);
+      };
+      if (!!obj.move || (!!obj.score && obj.scoreMsg == "Time")) {
+        // Security: only update moves if index is right,
+        // and score with scoreMsg "Time" if really lost on time.
+        let query =
+          "SELECT MAX(idx) AS maxIdx, MAX(played) AS lastPlayed " +
           "FROM Moves " +
           "WHERE gid = " + id;
-        db.get(query, (err,ret) => {
-          const m = obj.move;
-          if (!ret.maxIdx || ret.maxIdx + 1 == m.idx) {
-            query =
-              "INSERT INTO Moves (gid, squares, played, idx) VALUES " +
-              "(" + id + ",?," + m.played + "," + m.idx + ")";
-            db.run(query, JSON.stringify(m.squares));
-            cb(null);
+        db.get(query, (err, ret) => {
+          if (!!obj.move ) {
+            if (!ret.maxIdx || ret.maxIdx + 1 == obj.move.idx) {
+              query =
+                "INSERT INTO Moves (gid, squares, played, idx) VALUES " +
+                "(" + id + ",?," + Date.now() + "," + obj.move.idx + ")";
+              db.run(query, JSON.stringify(obj.move.squares));
+              finishAndSendQuery();
+            } else cb({ errmsg: "Wrong move index" });
+          } else {
+            if (ret.maxIdx < 2) cb({ errmsg: "Time not over" });
+            else {
+              // We also need the game cadence
+              query =
+                "SELECT cadence " +
+                "FROM Games " +
+                "WHERE id = " + id;
+              db.get(query, (err2, ret2) => {
+                const daysTc = parseInt(ret2.cadence.match(/\(^[0-9]+\)/)[0]);
+                if (Date.now() - ret.lastPlayed > daysTc * 24 * 3600 * 1000)
+                  finishAndSendQuery();
+                else cb({ errmsg: "Time not over" });
+              });
+            }
           }
-          else cb({errmsg:"Wrong move index"});
         });
-      }
-      else cb(null);
-      if (!!obj.chat)
-      {
-        query =
+      } else finishAndSendQuery();
+      // NOTE: chat and delchat are mutually exclusive
+      if (!!obj.chat) {
+        const query =
           "INSERT INTO Chats (gid, msg, name, added) VALUES ("
             + id + ",?,'" + obj.chat.name + "'," + Date.now() + ")";
         db.run(query, obj.chat.msg);
-      }
-      else if (obj.delchat)
-      {
-        query =
+      } else if (obj.delchat) {
+        const query =
           "DELETE " +
           "FROM Chats " +
           "WHERE gid = " + id;
@@ -368,7 +404,7 @@ const GameModel =
           "deletedBy" +
           (obj.deletedBy == 'w' ? "Black" : "White") +
           " AS deletedByOpp";
-        query =
+        const query =
           "SELECT " + selection + " " +
           "FROM Games " +
           "WHERE id = " + id;
diff --git a/server/routes/challenges.js b/server/routes/challenges.js
index d27f81ee..4c2d5b40 100644
--- a/server/routes/challenges.js
+++ b/server/routes/challenges.js
@@ -29,7 +29,7 @@ router.post("/challenges", access.logged, access.ajax, (req,res) => {
           if (user.notify)
             UserModel.notify(
               user,
-              "New challenge: " + params.siteURL + "/#/?disp=corr");
+              "New challenge : " + params.siteURL + "/#/?disp=corr");
         }
       });
     } else insertChallenge();
diff --git a/server/routes/games.js b/server/routes/games.js
index 77877a2e..42c15c99 100644
--- a/server/routes/games.js
+++ b/server/routes/games.js
@@ -13,12 +13,13 @@ router.post("/games", access.logged, access.ajax, (req,res) => {
   if (
     Array.isArray(gameInfo.players) &&
     gameInfo.players.some(p => p.id == req.userId) &&
-    (!cid || cid.toString().match(/^[0-9]+$/)) &&
+    (!cid || !!cid.toString().match(/^[0-9]+$/)) &&
     GameModel.checkGameInfo(gameInfo)
   ) {
     if (!!cid) ChallengeModel.remove(cid);
     GameModel.create(
-      gameInfo.vid, gameInfo.fen, gameInfo.cadence, gameInfo.players,
+      gameInfo.vid, gameInfo.fen, gameInfo.randomness,
+      gameInfo.cadence, gameInfo.players,
       (err, ret) => {
         const oppIdx = (gameInfo.players[0].id == req.userId ? 1 : 0);
         const oppId = gameInfo.players[oppIdx].id;
@@ -35,7 +36,7 @@ router.get("/games", access.ajax, (req,res) => {
   const gameId = req.query["gid"];
   if (!!gameId && gameId.match(/^[0-9]+$/)) {
     GameModel.getOne(gameId, (err, game) => {
-      res.json({ game: game });
+      res.json(err || { game: game });
     });
   }
 });
@@ -45,20 +46,20 @@ router.get("/observedgames", access.ajax, (req,res) => {
   const userId = req.query["uid"];
   const cursor = req.query["cursor"];
   if (!!userId.match(/^[0-9]+$/) && !!cursor.match(/^[0-9]+$/)) {
-    GameModel.getObserved(userId, (err, games) => {
+    GameModel.getObserved(userId, cursor, (err, games) => {
       res.json({ games: games });
     });
   }
 });
 
 // Get by user ID, for MyGames page
-router.get("/runninggames", access.ajax, access.logged, (req,res) => {
+router.get("/runninggames", access.logged, access.ajax, (req,res) => {
   GameModel.getRunning(req.userId, (err, games) => {
     res.json({ games: games });
   });
 });
 
-router.get("/completedgames", access.ajax, access.logged, (req,res) => {
+router.get("/completedgames", access.logged, access.ajax, (req,res) => {
   const cursor = req.query["cursor"];
   if (!!cursor.match(/^[0-9]+$/)) {
     GameModel.getCompleted(req.userId, cursor, (err, games) => {
@@ -72,7 +73,7 @@ router.put("/games", access.logged, access.ajax, (req,res) => {
   const gid = req.body.gid;
   let obj = req.body.newObj;
   if (gid.toString().match(/^[0-9]+$/) && GameModel.checkGameUpdate(obj)) {
-    GameModel.getPlayers(gid, (err,players) => {
+    GameModel.getPlayers(gid, (err, players) => {
       let myColor = '';
       if (players.white == req.userId) myColor = 'w';
       else if (players.black == req.userId) myColor = 'b';
@@ -88,8 +89,8 @@ router.put("/games", access.logged, access.ajax, (req,res) => {
             const oppid = (myColor == 'w' ? players.black : players.white);
             const messagePrefix =
               !!obj.move
-                ? "New move in game: "
-                : "Game ended: ";
+                ? "New move in game : "
+                : "Game ended : ";
             UserModel.tryNotify(
               oppid,
               messagePrefix + params.siteURL + "/#/game/" + gid
diff --git a/server/sockets.js b/server/sockets.js
index 7de7abb9..4907965d 100644
--- a/server/sockets.js
+++ b/server/sockets.js
@@ -32,24 +32,13 @@ module.exports = function(wss) {
     const id = query["id"];
     const tmpId = query["tmpId"];
     const page = query["page"];
-    const notifyRoom = (page,code,obj={}) => {
-      if (!clients[page]) return;
-      Object.keys(clients[page]).forEach(k => {
-        Object.keys(clients[page][k]).forEach(x => {
-          if (k == sid && x == tmpId) return;
-          send(
-            clients[page][k][x].socket,
-            Object.assign({ code: code, from: sid }, obj)
-          );
-        });
-      });
-    };
-    // For focus events: no need to target self
-    const notifyAllBut = (page,code,obj={},except) => {
+    const notifyRoom = (page, code, obj={}, except) => {
       if (!clients[page]) return;
+      except = except || [];
       Object.keys(clients[page]).forEach(k => {
         if (except.includes(k)) return;
         Object.keys(clients[page][k]).forEach(x => {
+          if (k == sid && x == tmpId) return;
           send(
             clients[page][k][x].socket,
             Object.assign({ code: code, from: sid }, obj)
@@ -208,14 +197,16 @@ module.exports = function(wss) {
         case "drawoffer":
         case "rematchoffer":
         case "draw":
-          notifyRoom(page, obj.code, {data: obj.data});
+          notifyRoom(page, obj.code, {data: obj.data}, obj.excluded);
           break;
 
         case "rnewgame":
           // A rematch game started:
-          // NOTE: no need to explicitely notify Hall: the game will be sent
-          notifyAllBut(page, "newgame", {data: obj.data}, [sid]);
-          notifyRoom("/mygames", "newgame", {data: obj.data});
+          notifyRoom(page, "newgame", {data: obj.data});
+          // Explicitely notify Hall if gametype == corr.
+          // Live games will be polled from Hall after gconnect event.
+          if (obj.data.cadence.indexOf('d') >= 0)
+            notifyRoom("/", "newgame", {data: obj.data});
           break;
 
         case "newmove": {
@@ -284,11 +275,11 @@ module.exports = function(wss) {
 
         case "getfocus":
         case "losefocus":
-          if (page == "/") notifyAllBut("/", obj.code, { page: "/" }, [sid]);
+          if (page == "/") notifyRoom("/", obj.code, { page: "/" }, [sid]);
           else {
             // Notify game room + Hall:
-            notifyAllBut(page, obj.code, {}, [sid]);
-            notifyAllBut("/", obj.code, { page: page }, [sid]);
+            notifyRoom(page, obj.code, {}, [sid]);
+            notifyRoom("/", obj.code, { page: page }, [sid]);
           }
           break;
 
-- 
2.44.0