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