Remove (useless?) pingback logic + retry send move on client side
[xogo.git] / server.js
index c21d8bf..7dbbaac 100644 (file)
--- a/server.js
+++ b/server.js
@@ -1,78 +1,75 @@
 const params = require("./parameters.js");
-const WebSocket = require('ws');
+const WebSocket = require("ws");
 const wss = new WebSocket.Server(
   {port: params.socket_port, path: params.socket_path});
 
 let challenges = {}; //variantName --> socketId, name
-let games = {}; //gameId --> gameInfo (vname, fen, players, options)
+let games = {}; //gameId --> gameInfo (vname, fen, players, options, time)
 let sockets = {}; //socketId --> socket
 const variants = require("./variants.js");
+const Crypto = require("crypto");
+const randstrSize = 8;
 
 const send = (sid, code, data) => {
   const socket = sockets[sid];
-  // If a player delete local infos and then try to resume a game,
+  // If a player deletes local infos and then tries to resume a game,
   // sockets[oppSid] will probably not exist anymore:
   if (socket) socket.send(JSON.stringify(Object.assign({ code: code }, data)));
-}
+};
 
-const Crypto = require('crypto')
-function randomString(size = 8) {
-  return Crypto.randomBytes(size).toString('hex').slice(0, size);
-}
-
-wss.on('connection', function connection(socket, req) {
+wss.on("connection", function connection(socket, req) {
   const sid = req.url.split("=")[1]; //...?sid=...
   sockets[sid] = socket;
   socket.isAlive = true;
-  socket.on('pong', () => socket.isAlive = true);
+  socket.on("pong", () => socket.isAlive = true);
 
   function launchGame(vname, players, options) {
-    const gid = randomString(8);
+    const gid =
+      Crypto.randomBytes(randstrSize).toString("hex").slice(0, randstrSize);
     games[gid] = {
       vname: vname,
       players: players.map(p => {
                  return (!p ? null : {sid: p.sid, name: p.name});
                }),
-      options: options
+      options: options,
+      time: Date.now()
     };
     if (players.every(p => p)) {
       const gameInfo = Object.assign(
         // Provide seed so that both players initialize with same FEN
         {seed: Math.floor(Math.random() * 1984), gid: gid},
         games[gid]);
-      for (let i of [0, 1]) {
-        send(players[i].sid, "gamestart",
-             Object.assign({randvar: players[i].randvar}, gameInfo));
+      for (const p of players) {
+        send(p.sid,
+             "gamestart",
+             Object.assign({randvar: p.randvar}, gameInfo));
       }
     }
     else {
       // Incomplete players array: do not start game yet
       send(sid, "gamecreated", {gid: gid});
-      // If nobody joins within a minute, delete game
+      // If nobody joins within 5 minutes, delete game
       setTimeout(
         () => {
           if (games[gid] && games[gid].players.some(p => !p))
             delete games[gid];
         },
-        60000
+        5 * 60000
       );
     }
   }
 
-  socket.on('message', (msg) => {
+  socket.on("message", (msg) => {
     const obj = JSON.parse(msg);
     switch (obj.code) {
       // Send challenge (may trigger game creation)
       case "seekgame": {
-        // Only one challenge per player:
-        if (Object.keys(challenges).some(k => challenges[k].sid == sid))
-          return;
         let opponent = undefined,
             choice = undefined;
         const vname = obj.vname,
               randvar = (obj.vname == "_random");
         if (vname == "_random") {
-          // Pick any current challenge if any
+          // Pick any current challenge if possible
           const currentChalls = Object.keys(challenges);
           if (currentChalls.length >= 1) {
             choice =
@@ -104,7 +101,7 @@ wss.on('connection', function connection(socket, req) {
           challenges[vname] = {sid: sid, name: obj.name, randvar: randvar};
         break;
       }
-      // Set FEN after game was created
+      // Set FEN after game was created (received twice)
       case "setfen":
         games[obj.gid].fen = obj.fen;
         break;
@@ -119,7 +116,7 @@ wss.on('connection', function connection(socket, req) {
         delete challenges[obj.vname];
         break;
       // Receive rematch
-      case "rematch": {
+      case "rematch":
         if (!games[obj.gid]) send(sid, "closerematch");
         else {
           const myIndex = (games[obj.gid].players[0].sid == sid ? 0 : 1);
@@ -133,19 +130,17 @@ wss.on('connection', function connection(socket, req) {
           }
         }
         break;
-      }
-        // Rematch cancellation
-      case "norematch": {
+      // Rematch cancellation
+      case "norematch":
         if (games[obj.gid]) {
           const myIndex = (games[obj.gid].players[0].sid == sid ? 0 : 1);
           send(games[obj.gid].players[1-myIndex].sid, "closerematch");
         }
         break;
-      }
       // Create game vs. friend
-      case "creategame":
+      case "creategame": {
         let players = [
-          { sid: obj.player.sid, name: obj.player.name },
+          {sid: obj.player.sid, name: obj.player.name},
           undefined
         ];
         if (
@@ -156,8 +151,9 @@ wss.on('connection', function connection(socket, req) {
         }
         launchGame(obj.vname, players, obj.options);
         break;
+      }
       // Join game vs. friend
-      case "joingame": {
+      case "joingame":
         if (!games[obj.gid]) send(sid, "jointoolate");
         else {
           // Join a game (started by some other player)
@@ -168,29 +164,28 @@ wss.on('connection', function connection(socket, req) {
             // Provide seed so that both players initialize with same FEN
             {seed: Math.floor(Math.random()*1984), gid: obj.gid},
             games[obj.gid]);
-          for (let i of [0, 1])
-            send(games[obj.gid].players[i].sid, "gamestart", gameInfo);
+          for (const p of games[obj.gid].players)
+            send(p.sid, "gamestart", gameInfo);
         }
         break;
-      }
       // Relay a move + update games object
-      case "newmove": {
-        // TODO?: "pingback" strategy to ensure that move was transmitted
+      case "newmove":
         games[obj.gid].fen = obj.fen;
+        games[obj.gid].time = Date.now(); //update timestamp in case of
         const playingWhite = (games[obj.gid].players[0].sid == sid);
         const oppSid = games[obj.gid].players[playingWhite ? 1 : 0].sid;
-        send(oppSid, "newmove", { moves: obj.moves });
+        send(oppSid, "newmove", {moves: obj.moves});
         break;
-      }
       // Relay "game ends" message
-      case "gameover": {
-        const playingWhite = (games[obj.gid].players[0].sid == sid);
-        const oppSid = games[obj.gid].players[playingWhite ? 1 : 0].sid;
-        if (obj.relay) send(oppSid, "gameover", { gid: obj.gid });
-        games[obj.gid].over = true;
-        setTimeout( () => delete games[obj.gid], 60000 );
+      case "gameover":
+        if (obj.relay) {
+          const playingWhite = (games[obj.gid].players[0].sid == sid);
+          const oppSid = games[obj.gid].players[playingWhite ? 1 : 0].sid;
+          send(oppSid, "gameover", { gid: obj.gid });
+        }
+        // 2 minutes timeout for rematch:
+        setTimeout(() => delete games[obj.gid], 2 * 60000);
         break;
-      }
     }
   });
   socket.on("close", () => {
@@ -204,11 +199,25 @@ wss.on('connection', function connection(socket, req) {
   });
 });
 
-const interval = setInterval(() => {
+const heartbeat = setInterval(() => {
   wss.clients.forEach((ws) => {
     if (ws.isAlive === false) return ws.terminate();
     ws.isAlive = false;
     ws.ping();
   });
 }, 30000);
-wss.on('close', () => clearInterval(interval));
+
+// Every 24 hours, scan games and remove if last move older than 24h
+const dayInMillisecs = 24 * 60 * 60 * 1000;
+const killOldGames = setInterval(() => {
+  const now = Date.now();
+  Object.keys(games).forEach(gid => {
+    if (now - games[gid].time >= dayInMillisecs) delete games[gid];
+  });
+}, dayInMillisecs);
+
+// TODO: useful code here?
+wss.on("close", () => {
+  clearInterval(heartbeat);
+  clearInterval(killOldGames);
+});