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