New variant idea
[xogo.git] / server.js
index 7dbbaac..06d9866 100644 (file)
--- a/server.js
+++ b/server.js
@@ -1,7 +1,9 @@
 const params = require("./parameters.js");
 const WebSocket = require("ws");
-const wss = new WebSocket.Server(
-  {port: params.socket_port, path: params.socket_path});
+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, time)
@@ -10,63 +12,64 @@ 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 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)));
-};
+  if (socket)
+    socket.send(JSON.stringify(Object.assign({code: code}, data)));
+}
+
+function initializeGame(vname, players, options) {
+  const gid =
+    Crypto.randomBytes(randstrSize).toString("hex").slice(0, randstrSize);
+  games[gid] = {
+    vname: vname,
+    players: players,
+    options: options,
+    time: Date.now(),
+    moveHash: {} //set of moves hashes seen so far
+  };
+  return gid;
+}
+
+// Provide seed in case of, so that both players initialize with same FEN
+function launchGame(gid) {
+  const gameInfo = Object.assign(
+    {seed: Math.floor(Math.random() * 19840), gid: gid},
+    games[gid]
+  );
+  // players array is supposed to be full:
+  for (const p of games[gid].players)
+    send(p.sid, "gamestart", gameInfo);
+}
 
-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);
-
-  function launchGame(vname, players, options) {
-    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,
-      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
-      );
-    }
+  if (params.dev == true) {
+    const chokidar = require("chokidar");
+    const watcher = chokidar.watch(
+      ["*.js", "*.css", "utils/", "variants/"],
+      {persistent: true});
+    watcher.on("change", path => send(sid, "filechange", {path: path}));
   }
-
   socket.on("message", (msg) => {
     const obj = JSON.parse(msg);
     switch (obj.code) {
       // Send challenge (may trigger game creation)
       case "seekgame": {
-        let opponent = undefined,
-            choice = undefined;
-        const vname = obj.vname,
+        let oppIndex = undefined, //variant name
+            choice = undefined; //variant finally played
+        const vname = obj.vname, //variant requested
               randvar = (obj.vname == "_random");
         if (vname == "_random") {
           // Pick any current challenge if possible
@@ -74,27 +77,32 @@ wss.on("connection", function connection(socket, req) {
           if (currentChalls.length >= 1) {
             choice =
               currentChalls[Math.floor(Math.random() * currentChalls.length)];
-            opponent = challenges[choice];
+            oppIndex = choice;
           }
         }
         else if (challenges[vname]) {
-          opponent = challenges[vname];
+          // Anyone wanting to play the same variant ?
           choice = vname;
+          oppIndex = vname;
         }
-        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;
-          }
+        else if (challenges["_random"]) {
+          // Anyone accepting any variant (including vname) ?
+          choice = vname;
+          oppIndex = "_random";
+        }
+        if (oppIndex) {
+          if (choice == "_random")
+            choice = getRandomVariant();
           // Launch game
           let players = [
             {sid: sid, name: obj.name, randvar: randvar},
-            opponent
+            Object.assign({}, challenges[oppIndex])
           ];
-          if (Math.random() < 0.5) players = players.reverse();
-          launchGame(choice, players, {}); //empty options => default
+          delete challenges[oppIndex];
+          if (Math.random() < 0.5)
+            players = players.reverse();
+          // Empty options = default
+          launchGame( initializeGame(choice, players, {}) );
         }
         else
           // Place challenge and wait. 'randvar' indicate if we play anything
@@ -106,27 +114,36 @@ wss.on("connection", function connection(socket, req) {
         games[obj.gid].fen = obj.fen;
         break;
       // Send back game informations
-      case "getgame": {
-        if (!games[obj.gid]) send(sid, "nogame");
-        else send(sid, "gameinfo", games[obj.gid]);
+      case "getgame":
+        if (!games[obj.gid])
+          send(sid, "nogame");
+        else
+          send(sid, "gameinfo", games[obj.gid]);
         break;
-      }
       // Cancel challenge
       case "cancelseek":
         delete challenges[obj.vname];
         break;
       // Receive rematch
       case "rematch":
-        if (!games[obj.gid]) send(sid, "closerematch");
+        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,
-                       games[obj.gid].players.reverse(),
-                       games[obj.gid].options);
+            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);
+            const gid = initializeGame(vname,
+                                       games[obj.gid].players.reverse(),
+                                       games[obj.gid].options);
+            launchGame(gid);
           }
         }
         break;
@@ -149,29 +166,45 @@ wss.on("connection", function connection(socket, req) {
         ) {
           players = players.reverse();
         }
-        launchGame(obj.vname, players, obj.options);
+        // Incomplete players array: do not start game yet
+        const gid = initializeGame(obj.vname, players, obj.options);
+        send(sid, "gamecreated", {gid: gid});
+        // If nobody joins within 3 minutes, delete game
+        setTimeout(
+          () => {
+            if (games[gid] && games[gid].players.some(p => !p))
+              delete games[gid];
+          },
+          3 * 60000
+        );
         break;
       }
       // Join game vs. friend
       case "joingame":
-        if (!games[obj.gid]) send(sid, "jointoolate");
+        if (!games[obj.gid])
+          send(sid, "jointoolate");
         else {
-          // Join a game (started by some other player)
           const emptySlot = games[obj.gid].players.findIndex(p => !p);
-          if (emptySlot < 0) send(sid, "jointoolate");
-          games[obj.gid].players[emptySlot] = {sid: sid, name: obj.name};
-          const gameInfo = Object.assign(
-            // Provide seed so that both players initialize with same FEN
-            {seed: Math.floor(Math.random()*1984), gid: obj.gid},
-            games[obj.gid]);
-          for (const p of games[obj.gid].players)
-            send(p.sid, "gamestart", gameInfo);
+          if (emptySlot < 0)
+            send(sid, "jointoolate");
+          else {
+            // Join a game (started by some other player)
+            games[obj.gid].players[emptySlot] = {sid: sid, name: obj.name};
+            launchGame(obj.gid);
+          }
         }
         break;
       // Relay a move + update games object
       case "newmove":
+        // NOTE: still potential racing issues, but... fingers crossed
+        const hash = Crypto.createHash("md5")
+                     .update(JSON.stringify(obj.fen))
+                     .digest("hex");
+        if (games[obj.gid].moveHash[hash])
+          break;
+        games[obj.gid].moveHash[hash] = true;
         games[obj.gid].fen = obj.fen;
-        games[obj.gid].time = Date.now(); //update timestamp in case of
+        games[obj.gid].time = Date.now(); //update useful if verrry slow game
         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});
@@ -196,12 +229,20 @@ 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 && p.sid == sid);
+      if (myIndex >= 0) {
+        if (g.rematch && g.rematch[myIndex] > 0) g.rematch[myIndex] = 0;
+        break; //only one game per player
+      }
+    }
   });
 });
 
 const heartbeat = setInterval(() => {
   wss.clients.forEach((ws) => {
-    if (ws.isAlive === false) return ws.terminate();
+    if (ws.isAlive === false)
+      return ws.terminate();
     ws.isAlive = false;
     ws.ping();
   });
@@ -212,7 +253,8 @@ 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];
+    if (now - games[gid].time >= dayInMillisecs)
+      delete games[gid];
   });
 }, dayInMillisecs);