Cosmetics
[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 wss.on("connection", (socket, req) => {
21 const sid = req.url.split("=")[1]; //...?sid=...
22 sockets[sid] = socket;
23 socket.isAlive = true;
24 socket.on("pong", () => socket.isAlive = true);
25
26 function launchGame(vname, players, options) {
27 const gid =
28 Crypto.randomBytes(randstrSize).toString("hex").slice(0, randstrSize);
29 games[gid] = {
30 vname: vname,
31 players: players.map(p => {
32 return (!p ? null : {sid: p.sid, name: p.name});
33 }),
34 options: options,
35 time: Date.now()
36 };
37 if (players.every(p => p)) {
38 const gameInfo = Object.assign(
39 // Provide seed so that both players initialize with same FEN
40 {seed: Math.floor(Math.random() * 1984), gid: gid},
41 games[gid]);
42 for (const p of players) {
43 send(p.sid,
44 "gamestart",
45 Object.assign({randvar: p.randvar}, gameInfo));
46 }
47 }
48 else {
49 // Incomplete players array: do not start game yet
50 send(sid, "gamecreated", {gid: gid});
51 // If nobody joins within 5 minutes, delete game
52 setTimeout(
53 () => {
54 if (games[gid] && games[gid].players.some(p => !p))
55 delete games[gid];
56 },
57 5 * 60000
58 );
59 }
60 }
61
62 socket.on("message", (msg) => {
63 const obj = JSON.parse(msg);
64 switch (obj.code) {
65 // Send challenge (may trigger game creation)
66 case "seekgame": {
67 let opponent = undefined,
68 choice = undefined;
69 const vname = obj.vname,
70 randvar = (obj.vname == "_random");
71 if (vname == "_random") {
72 // Pick any current challenge if possible
73 const currentChalls = Object.keys(challenges);
74 if (currentChalls.length >= 1) {
75 choice =
76 currentChalls[Math.floor(Math.random() * currentChalls.length)];
77 opponent = challenges[choice];
78 }
79 }
80 else if (challenges[vname]) {
81 opponent = challenges[vname];
82 choice = vname;
83 }
84 if (opponent) {
85 delete challenges[choice];
86 if (choice == "_random") {
87 // Pick a variant at random in the list
88 const index = Math.floor(Math.random() * variants.length);
89 choice = variants[index].name;
90 }
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 = [false, false];
124 games[obj.gid].rematch[myIndex] = true;
125 if (games[obj.gid].rematch[1-myIndex]) {
126 // Launch new game, colors reversed
127 launchGame(games[obj.gid].vname,
128 games[obj.gid].players.reverse(),
129 games[obj.gid].options);
130 }
131 }
132 break;
133 // Rematch cancellation
134 case "norematch":
135 if (games[obj.gid]) {
136 const myIndex = (games[obj.gid].players[0].sid == sid ? 0 : 1);
137 send(games[obj.gid].players[1-myIndex].sid, "closerematch");
138 }
139 break;
140 // Create game vs. friend
141 case "creategame": {
142 let players = [
143 {sid: obj.player.sid, name: obj.player.name},
144 undefined
145 ];
146 if (
147 obj.player.color == 'b' ||
148 (obj.player.color == '' && Math.random() < 0.5)
149 ) {
150 players = players.reverse();
151 }
152 launchGame(obj.vname, players, obj.options);
153 break;
154 }
155 // Join game vs. friend
156 case "joingame":
157 if (!games[obj.gid]) send(sid, "jointoolate");
158 else {
159 // Join a game (started by some other player)
160 const emptySlot = games[obj.gid].players.findIndex(p => !p);
161 if (emptySlot < 0) send(sid, "jointoolate");
162 games[obj.gid].players[emptySlot] = {sid: sid, name: obj.name};
163 const gameInfo = Object.assign(
164 // Provide seed so that both players initialize with same FEN
165 {seed: Math.floor(Math.random()*1984), gid: obj.gid},
166 games[obj.gid]);
167 for (const p of games[obj.gid].players)
168 send(p.sid, "gamestart", gameInfo);
169 }
170 break;
171 // Relay a move + update games object
172 case "newmove":
173 games[obj.gid].fen = obj.fen;
174 games[obj.gid].time = Date.now(); //update timestamp in case of
175 const playingWhite = (games[obj.gid].players[0].sid == sid);
176 const oppSid = games[obj.gid].players[playingWhite ? 1 : 0].sid;
177 send(oppSid, "newmove", {moves: obj.moves});
178 break;
179 // Relay "game ends" message
180 case "gameover":
181 if (obj.relay) {
182 const playingWhite = (games[obj.gid].players[0].sid == sid);
183 const oppSid = games[obj.gid].players[playingWhite ? 1 : 0].sid;
184 send(oppSid, "gameover", { gid: obj.gid });
185 }
186 // 2 minutes timeout for rematch:
187 setTimeout(() => delete games[obj.gid], 2 * 60000);
188 break;
189 }
190 });
191 socket.on("close", () => {
192 delete sockets[sid];
193 for (const [key, value] of Object.entries(challenges)) {
194 if (value.sid == sid) {
195 delete challenges[key];
196 break; //only one challenge per player
197 }
198 }
199 });
200 });
201
202 const heartbeat = setInterval(() => {
203 wss.clients.forEach((ws) => {
204 if (ws.isAlive === false) return ws.terminate();
205 ws.isAlive = false;
206 ws.ping();
207 });
208 }, 30000);
209
210 // Every 24 hours, scan games and remove if last move older than 24h
211 const dayInMillisecs = 24 * 60 * 60 * 1000;
212 const killOldGames = setInterval(() => {
213 const now = Date.now();
214 Object.keys(games).forEach(gid => {
215 if (now - games[gid].time >= dayInMillisecs) delete games[gid];
216 });
217 }, dayInMillisecs);
218
219 // TODO: useful code here?
220 wss.on("close", () => {
221 clearInterval(heartbeat);
222 clearInterval(killOldGames);
223 });