9c85eeb5b43cbfbb1f9bcd3b95beaeed6a16a01f
[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.map(p => {
26 return (!p ? null : {sid: p.sid, name: p.name});
27 }),
28 options: options,
29 time: Date.now()
30 };
31 if (players.every(p => p)) {
32 const gameInfo = Object.assign(
33 // Provide seed so that both players initialize with same FEN
34 {seed: Math.floor(Math.random() * 1984), gid: gid},
35 games[gid]);
36 for (const p of players) {
37 send(p.sid,
38 "gamestart",
39 Object.assign({randvar: p.randvar}, gameInfo));
40 }
41 }
42 else {
43 // Incomplete players array: do not start game yet
44 send(sid, "gamecreated", {gid: gid});
45 // If nobody joins within 5 minutes, delete game
46 setTimeout(
47 () => {
48 if (games[gid] && games[gid].players.some(p => !p))
49 delete games[gid];
50 },
51 5 * 60000
52 );
53 }
54 }
55
56 function getRandomVariant() {
57 // Pick a variant at random in the list
58 const index = Math.floor(Math.random() * variants.length);
59 return variants[index].name;
60 }
61
62 wss.on("connection", (socket, req) => {
63 const sid = req.url.split("=")[1]; //...?sid=...
64 sockets[sid] = socket;
65 socket.isAlive = true;
66 socket.on("pong", () => socket.isAlive = true);
67
68 socket.on("message", (msg) => {
69 const obj = JSON.parse(msg);
70 switch (obj.code) {
71 // Send challenge (may trigger game creation)
72 case "seekgame": {
73 let opponent = undefined,
74 choice = undefined;
75 const vname = obj.vname,
76 randvar = (obj.vname == "_random");
77 if (vname == "_random") {
78 // Pick any current challenge if possible
79 const currentChalls = Object.keys(challenges);
80 if (currentChalls.length >= 1) {
81 choice =
82 currentChalls[Math.floor(Math.random() * currentChalls.length)];
83 opponent = challenges[choice];
84 }
85 }
86 else if (challenges[vname]) {
87 opponent = challenges[vname];
88 choice = vname;
89 }
90 if (opponent) {
91 delete challenges[choice];
92 if (choice == "_random") choice = getRandomVariant();
93 // Launch game
94 let players = [
95 {sid: sid, name: obj.name, randvar: randvar},
96 opponent
97 ];
98 if (Math.random() < 0.5) players = players.reverse();
99 launchGame(choice, players, {}); //empty options => default
100 }
101 else
102 // Place challenge and wait. 'randvar' indicate if we play anything
103 challenges[vname] = {sid: sid, name: obj.name, randvar: randvar};
104 break;
105 }
106 // Set FEN after game was created (received twice)
107 case "setfen":
108 games[obj.gid].fen = obj.fen;
109 break;
110 // Send back game informations
111 case "getgame": {
112 if (!games[obj.gid]) send(sid, "nogame");
113 else send(sid, "gameinfo", games[obj.gid]);
114 break;
115 }
116 // Cancel challenge
117 case "cancelseek":
118 delete challenges[obj.vname];
119 break;
120 // Receive rematch
121 case "rematch":
122 if (!games[obj.gid]) send(sid, "closerematch");
123 else {
124 const myIndex = (games[obj.gid].players[0].sid == sid ? 0 : 1);
125 if (!games[obj.gid].rematch) games[obj.gid].rematch = [0, 0];
126 games[obj.gid].rematch[myIndex] = !obj.random ? 1 : 2;
127 if (games[obj.gid].rematch[1-myIndex]) {
128 // Launch new game, colors reversed
129 let vname = games[obj.gid].vname;
130 if (games[obj.gid].rematch.every(r => r == 2))
131 vname = getRandomVariant();
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 });
205 });
206
207 const heartbeat = setInterval(() => {
208 wss.clients.forEach((ws) => {
209 if (ws.isAlive === false) return ws.terminate();
210 ws.isAlive = false;
211 ws.ping();
212 });
213 }, 30000);
214
215 // Every 24 hours, scan games and remove if last move older than 24h
216 const dayInMillisecs = 24 * 60 * 60 * 1000;
217 const killOldGames = setInterval(() => {
218 const now = Date.now();
219 Object.keys(games).forEach(gid => {
220 if (now - games[gid].time >= dayInMillisecs) delete games[gid];
221 });
222 }, dayInMillisecs);
223
224 // TODO: useful code here?
225 wss.on("close", () => {
226 clearInterval(heartbeat);
227 clearInterval(killOldGames);
228 });