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