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