From 8a9f61cec20509fd4398169d4ce1da73157d32ab Mon Sep 17 00:00:00 2001
From: Benjamin Auder <benjamin.auder@somewhere>
Date: Mon, 15 Nov 2021 02:38:47 +0100
Subject: [PATCH] Attempt to prevent 'lost moves'...

---
 app.js    | 49 ++++++++++++++++++++++++++++++++++++++-----------
 server.js | 45 +++++++++++++++++++++++++++++++++------------
 2 files changed, 71 insertions(+), 23 deletions(-)

diff --git a/app.js b/app.js
index 592d2a9..a46ceb3 100644
--- a/app.js
+++ b/app.js
@@ -295,6 +295,9 @@ const messageCenter = (msg) => {
       break;
     // Receive opponent's move:
     case "newmove":
+      send("gotmove", {fen: obj.fen, gid: gid});
+      if (obj.fen == lastFen) break; //got this move already
+      lastFen = obj.fen;
       if (document.hidden) notifyMe("move");
       vr.playReceivedMove(obj.moves, () => {
         if (vr.getCurrentScore(obj.moves[obj.moves.length-1]) != "*") {
@@ -304,6 +307,16 @@ const messageCenter = (msg) => {
         else toggleTurnIndicator(true);
       });
       break;
+    // The server notifies that it got our move:
+    case "gotmove":
+      if (obj.fen == lastFen) {
+        curMoves = [];
+        clearTimeout(timeout1);
+        clearTimeout(timeout2);
+        clearTimeout(timeout3);
+        callbackAfterConfirmation();
+      }
+      break;
     // Opponent stopped game (draw, abort, resign...)
     case "gameover":
       toggleVisible("gameStopped");
@@ -376,25 +389,39 @@ function notifyMe(code) {
   }
 }
 
-let curMoves = [];
-const afterPlay = (move) => { //pack into one moves array, then send
+let curMoves = [],
+    lastFen, lastMove,
+    timeout1, timeout2, timeout3;
+const callbackAfterConfirmation = () => {
+  const result = vr.getCurrentScore(lastMove);
+  if (result != "*") {
+    setTimeout( () => {
+      toggleVisible("gameStopped");
+      send("gameover", { gid: gid });
+    }, 2000);
+  }
+};
+const afterPlay = (move) => {
+  // Pack into one moves array, then send
   curMoves.push({
     appear: move.appear,
     vanish: move.vanish,
     start: move.start,
     end: move.end
   });
+  lastMove = move;
   if (vr.turn != playerColor) {
     toggleTurnIndicator(false);
-    send("newmove", { gid: gid, moves: curMoves, fen: vr.getFen() });
-    curMoves = [];
-    const result = vr.getCurrentScore(move);
-    if (result != "*") {
-      setTimeout( () => {
-        toggleVisible("gameStopped");
-        send("gameover", { gid: gid });
-      }, 2000);
-    }
+    lastFen = vr.getFen();
+    const sendMove =
+      () => send("newmove", {gid: gid, moves: curMoves, fen: lastFen});
+    // Send move until we obtain confirmation or timeout, then callback
+    sendMove();
+    timeout1 = setTimeout(sendMove, 500);
+    timeout2 = setTimeout(sendMove, 1500);
+    timeout3 = setTimeout(
+      () => alert("The move may be lost :( Please reload"),
+      3000);
   }
 };
 
diff --git a/server.js b/server.js
index c21d8bf..d8a2efa 100644
--- a/server.js
+++ b/server.js
@@ -6,8 +6,19 @@ const wss = new WebSocket.Server(
 let challenges = {}; //variantName --> socketId, name
 let games = {}; //gameId --> gameInfo (vname, fen, players, options)
 let sockets = {}; //socketId --> socket
+let sendmoveTimeout1 = {},
+    sendmoveTimeout2 = {},
+    sendmoveRetry = {},
+    stopRetry = {};
 const variants = require("./variants.js");
 
+const clearTrySendMove = (gid) => {
+  clearTimeout(sendmoveTimeout1[gid]);
+  clearTimeout(sendmoveTimeout2[gid]);
+  clearInterval(sendmoveRetry[gid]);
+  clearTimeout(stopRetry[gid]);
+};
+
 const send = (sid, code, data) => {
   const socket = sockets[sid];
   // If a player delete local infos and then try to resume a game,
@@ -119,7 +130,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,17 +144,15 @@ 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 },
           undefined
@@ -156,8 +165,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)
@@ -172,16 +182,27 @@ wss.on('connection', function connection(socket, req) {
             send(games[obj.gid].players[i].sid, "gamestart", gameInfo);
         }
         break;
-      }
       // Relay a move + update games object
-      case "newmove": {
-        // TODO?: "pingback" strategy to ensure that move was transmitted
+      case "newmove":
+        // If already received this move: skip
+        if (games[obj.gid].fen == obj.fen) break;
+        // Notify sender that the move is received:
+        send(sid, "gotmove", {fen: obj.fen});
         games[obj.gid].fen = obj.fen;
         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 });
+        const sendMove =
+          // NOTE: sending FEN also, to check it in "gotmove" below
+          () => send(oppSid, "newmove", {moves: obj.moves, fen: obj.fen});
+        sendMove();
+        sendmoveTimeout1[obj.gid] = setTimeout(sendMove, 500);
+        sendmoveTimeout2[obj.gid] = setTimeout(sendMove, 1500);
+        sendmoveRetry[obj.gid] = setInterval(sendMove, 5000);
+        stopRetry[obj.gid] = setTimeout(clearTrySendMove, 31000);
+        break;
+      case "gotmove":
+        if (games[obj.gid].fen == obj.fen) clearTrySendMove(obj.gid);
         break;
-      }
       // Relay "game ends" message
       case "gameover": {
         const playingWhite = (games[obj.gid].players[0].sid == sid);
-- 
2.44.0