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