Attempt to prevent 'lost moves'...
[xogo.git] / server.js
1 const params = require("./parameters.js");
2 const WebSocket = require('ws');
3 const wss = new WebSocket.Server(
4 {port: params.socket_port, path: params.socket_path});
5
6 let challenges = {}; //variantName --> socketId, name
7 let games = {}; //gameId --> gameInfo (vname, fen, players, options)
8 let sockets = {}; //socketId --> socket
9 let sendmoveTimeout1 = {},
10 sendmoveTimeout2 = {},
11 sendmoveRetry = {},
12 stopRetry = {};
13 const variants = require("./variants.js");
14
15 const clearTrySendMove = (gid) => {
16 clearTimeout(sendmoveTimeout1[gid]);
17 clearTimeout(sendmoveTimeout2[gid]);
18 clearInterval(sendmoveRetry[gid]);
19 clearTimeout(stopRetry[gid]);
20 };
21
22 const send = (sid, code, data) => {
23 const socket = sockets[sid];
24 // If a player delete local infos and then try to resume a game,
25 // sockets[oppSid] will probably not exist anymore:
26 if (socket) socket.send(JSON.stringify(Object.assign({ code: code }, data)));
27 }
28
29 const Crypto = require('crypto')
30 function randomString(size = 8) {
31 return Crypto.randomBytes(size).toString('hex').slice(0, size);
32 }
33
34 wss.on('connection', function connection(socket, req) {
35 const sid = req.url.split("=")[1]; //...?sid=...
36 sockets[sid] = socket;
37 socket.isAlive = true;
38 socket.on('pong', () => socket.isAlive = true);
39
40 function launchGame(vname, players, options) {
41 const gid = randomString(8);
42 games[gid] = {
43 vname: vname,
44 players: players.map(p => {
45 return (!p ? null : {sid: p.sid, name: p.name});
46 }),
47 options: options
48 };
49 if (players.every(p => p)) {
50 const gameInfo = Object.assign(
51 // Provide seed so that both players initialize with same FEN
52 {seed: Math.floor(Math.random() * 1984), gid: gid},
53 games[gid]);
54 for (let i of [0, 1]) {
55 send(players[i].sid, "gamestart",
56 Object.assign({randvar: players[i].randvar}, gameInfo));
57 }
58 }
59 else {
60 // Incomplete players array: do not start game yet
61 send(sid, "gamecreated", {gid: gid});
62 // If nobody joins within a minute, delete game
63 setTimeout(
64 () => {
65 if (games[gid] && games[gid].players.some(p => !p))
66 delete games[gid];
67 },
68 60000
69 );
70 }
71 }
72
73 socket.on('message', (msg) => {
74 const obj = JSON.parse(msg);
75 switch (obj.code) {
76 // Send challenge (may trigger game creation)
77 case "seekgame": {
78 // Only one challenge per player:
79 if (Object.keys(challenges).some(k => challenges[k].sid == sid))
80 return;
81 let opponent = undefined,
82 choice = undefined;
83 const vname = obj.vname,
84 randvar = (obj.vname == "_random");
85 if (vname == "_random") {
86 // Pick any current challenge if any
87 const currentChalls = Object.keys(challenges);
88 if (currentChalls.length >= 1) {
89 choice =
90 currentChalls[Math.floor(Math.random() * currentChalls.length)];
91 opponent = challenges[choice];
92 }
93 }
94 else if (challenges[vname]) {
95 opponent = challenges[vname];
96 choice = vname;
97 }
98 if (opponent) {
99 delete challenges[choice];
100 if (choice == "_random") {
101 // Pick a variant at random in the list
102 const index = Math.floor(Math.random() * variants.length);
103 choice = variants[index].name;
104 }
105 // Launch game
106 let players = [
107 {sid: sid, name: obj.name, randvar: randvar},
108 opponent
109 ];
110 if (Math.random() < 0.5) players = players.reverse();
111 launchGame(choice, players, {}); //empty options => default
112 }
113 else
114 // Place challenge and wait. 'randvar' indicate if we play anything
115 challenges[vname] = {sid: sid, name: obj.name, randvar: randvar};
116 break;
117 }
118 // Set FEN after game was created
119 case "setfen":
120 games[obj.gid].fen = obj.fen;
121 break;
122 // Send back game informations
123 case "getgame": {
124 if (!games[obj.gid]) send(sid, "nogame");
125 else send(sid, "gameinfo", games[obj.gid]);
126 break;
127 }
128 // Cancel challenge
129 case "cancelseek":
130 delete challenges[obj.vname];
131 break;
132 // Receive rematch
133 case "rematch":
134 if (!games[obj.gid]) send(sid, "closerematch");
135 else {
136 const myIndex = (games[obj.gid].players[0].sid == sid ? 0 : 1);
137 if (!games[obj.gid].rematch) games[obj.gid].rematch = [false, false];
138 games[obj.gid].rematch[myIndex] = true;
139 if (games[obj.gid].rematch[1-myIndex]) {
140 // Launch new game, colors reversed
141 launchGame(games[obj.gid].vname,
142 games[obj.gid].players.reverse(),
143 games[obj.gid].options);
144 }
145 }
146 break;
147 // Rematch cancellation
148 case "norematch":
149 if (games[obj.gid]) {
150 const myIndex = (games[obj.gid].players[0].sid == sid ? 0 : 1);
151 send(games[obj.gid].players[1-myIndex].sid, "closerematch");
152 }
153 break;
154 // Create game vs. friend
155 case "creategame": {
156 let players = [
157 { sid: obj.player.sid, name: obj.player.name },
158 undefined
159 ];
160 if (
161 obj.player.color == 'b' ||
162 (obj.player.color == '' && Math.random() < 0.5)
163 ) {
164 players = players.reverse();
165 }
166 launchGame(obj.vname, players, obj.options);
167 break;
168 }
169 // Join game vs. friend
170 case "joingame":
171 if (!games[obj.gid]) send(sid, "jointoolate");
172 else {
173 // Join a game (started by some other player)
174 const emptySlot = games[obj.gid].players.findIndex(p => !p);
175 if (emptySlot < 0) send(sid, "jointoolate");
176 games[obj.gid].players[emptySlot] = {sid: sid, name: obj.name};
177 const gameInfo = Object.assign(
178 // Provide seed so that both players initialize with same FEN
179 {seed: Math.floor(Math.random()*1984), gid: obj.gid},
180 games[obj.gid]);
181 for (let i of [0, 1])
182 send(games[obj.gid].players[i].sid, "gamestart", gameInfo);
183 }
184 break;
185 // Relay a move + update games object
186 case "newmove":
187 // If already received this move: skip
188 if (games[obj.gid].fen == obj.fen) break;
189 // Notify sender that the move is received:
190 send(sid, "gotmove", {fen: obj.fen});
191 games[obj.gid].fen = obj.fen;
192 const playingWhite = (games[obj.gid].players[0].sid == sid);
193 const oppSid = games[obj.gid].players[playingWhite ? 1 : 0].sid;
194 const sendMove =
195 // NOTE: sending FEN also, to check it in "gotmove" below
196 () => send(oppSid, "newmove", {moves: obj.moves, fen: obj.fen});
197 sendMove();
198 sendmoveTimeout1[obj.gid] = setTimeout(sendMove, 500);
199 sendmoveTimeout2[obj.gid] = setTimeout(sendMove, 1500);
200 sendmoveRetry[obj.gid] = setInterval(sendMove, 5000);
201 stopRetry[obj.gid] = setTimeout(clearTrySendMove, 31000);
202 break;
203 case "gotmove":
204 if (games[obj.gid].fen == obj.fen) clearTrySendMove(obj.gid);
205 break;
206 // Relay "game ends" message
207 case "gameover": {
208 const playingWhite = (games[obj.gid].players[0].sid == sid);
209 const oppSid = games[obj.gid].players[playingWhite ? 1 : 0].sid;
210 if (obj.relay) send(oppSid, "gameover", { gid: obj.gid });
211 games[obj.gid].over = true;
212 setTimeout( () => delete games[obj.gid], 60000 );
213 break;
214 }
215 }
216 });
217 socket.on("close", () => {
218 delete sockets[sid];
219 for (const [key, value] of Object.entries(challenges)) {
220 if (value.sid == sid) {
221 delete challenges[key];
222 break; //only one challenge per player
223 }
224 }
225 });
226 });
227
228 const interval = setInterval(() => {
229 wss.clients.forEach((ws) => {
230 if (ws.isAlive === false) return ws.terminate();
231 ws.isAlive = false;
232 ws.ping();
233 });
234 }, 30000);
235 wss.on('close', () => clearInterval(interval));