Some improvements + simplify TODOs
[xogo.git] / server.js
index c21d8bf..37a343e 100644 (file)
--- a/server.js
+++ b/server.js
@@ -1,78 +1,79 @@
 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) => {
+function 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);
+function launchGame(vname, players, options) {
+  const gid =
+    Crypto.randomBytes(randstrSize).toString("hex").slice(0, randstrSize);
+  games[gid] = {
+    vname: vname,
+    players: players,
+    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 (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 5 minutes, delete game
+    setTimeout(
+      () => {
+        if (games[gid] && games[gid].players.some(p => !p))
+          delete games[gid];
+      },
+      5 * 60000
+    );
+  }
 }
 
-wss.on('connection', function connection(socket, req) {
+function getRandomVariant() {
+  // Pick a variant at random in the list
+  const index = Math.floor(Math.random() * variants.length);
+  return variants[index].name;
+}
+
+wss.on("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);
-    games[gid] = {
-      vname: vname,
-      players: players.map(p => {
-                 return (!p ? null : {sid: p.sid, name: p.name});
-               }),
-      options: options
-    };
-    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));
-      }
-    }
-    else {
-      // Incomplete players array: do not start game yet
-      send(sid, "gamecreated", {gid: gid});
-      // If nobody joins within a minute, delete game
-      setTimeout(
-        () => {
-          if (games[gid] && games[gid].players.some(p => !p))
-            delete games[gid];
-        },
-        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 =
@@ -86,11 +87,7 @@ wss.on('connection', function connection(socket, req) {
         }
         if (opponent) {
           delete challenges[choice];
-          if (choice == "_random") {
-            // Pick a variant at random in the list
-            const index = Math.floor(Math.random() * variants.length);
-            choice = variants[index].name;
-          }
+          if (choice == "_random") choice = getRandomVariant();
           // Launch game
           let players = [
             {sid: sid, name: obj.name, randvar: randvar},
@@ -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,33 +116,36 @@ 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);
-          if (!games[obj.gid].rematch) games[obj.gid].rematch = [false, false];
-          games[obj.gid].rematch[myIndex] = true;
+          if (!games[obj.gid].rematch) games[obj.gid].rematch = [0, 0];
+          games[obj.gid].rematch[myIndex] = !obj.random ? 1 : 2;
           if (games[obj.gid].rematch[1-myIndex]) {
             // Launch new game, colors reversed
-            launchGame(games[obj.gid].vname,
+            let vname = games[obj.gid].vname;
+            const allrand = games[obj.gid].rematch.every(r => r == 2);
+            if (allrand) vname = getRandomVariant();
+            games[obj.gid].players.forEach(p =>
+              p.randvar = allrand ? true : false);
+            launchGame(vname,
                        games[obj.gid].players.reverse(),
                        games[obj.gid].options);
           }
         }
         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 +156,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 +169,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", () => {
@@ -201,14 +201,35 @@ wss.on('connection', function connection(socket, req) {
         break; //only one challenge per player
       }
     }
+    for (let g of Object.values(games)) {
+      const myIndex = g.players.findIndex(p => p.sid == sid);
+      if (myIndex >= 0) {
+        if (g.rematch && g.rematch[myIndex] > 0) g.rematch[myIndex] = 0;
+        break; //only one game per player
+      }
+    }
   });
 });
 
-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);
+});