First commit
[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)
8 let sockets = {}; //socketId --> socket
9 const variants = require("./variants.js");
10
11 const send = (sid, code, data) => {
12 const socket = sockets[sid];
13 // If a player delete local infos and then try to resume a game,
14 // sockets[oppSid] will probably not exist anymore:
15 if (socket) socket.send(JSON.stringify(Object.assign({ code: code }, data)));
16 }
17
18 const Crypto = require('crypto')
19 function randomString(size = 8) {
20 return Crypto.randomBytes(size).toString('hex').slice(0, size);
21 }
22
23 wss.on('connection', function connection(socket, req) {
24 const sid = req.url.split("=")[1]; //...?sid=...
25 sockets[sid] = socket;
26 socket.isAlive = true;
27 socket.on('pong', () => socket.isAlive = true);
28
29 function launchGame(vname, players, options) {
30 const gid = randomString(8);
31 games[gid] = {
32 vname: vname,
33 players: players.map(p => {
34 return (!p ? null : {sid: p.sid, name: p.name});
35 }),
36 options: options
37 };
38 if (players.every(p => p)) {
39 const gameInfo = Object.assign(
40 // Provide seed so that both players initialize with same FEN
41 {seed: Math.floor(Math.random() * 1984), gid: gid},
42 games[gid]);
43 for (let i of [0, 1]) {
44 send(players[i].sid, "gamestart",
45 Object.assign({randvar: players[i].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 a minute, delete game
52 setTimeout(
53 () => {
54 if (games[gid] && games[gid].players.some(p => !p))
55 delete games[gid];
56 },
57 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 // Only one challenge per player:
68 if (Object.keys(challenges).some(k => challenges[k].sid == sid))
69 return;
70 let opponent = undefined,
71 choice = undefined;
72 const vname = obj.vname,
73 randvar = (obj.vname == "_random");
74 if (vname == "_random") {
75 // Pick any current challenge if any
76 const currentChalls = Object.keys(challenges);
77 if (currentChalls.length >= 1) {
78 choice =
79 currentChalls[Math.floor(Math.random() * currentChalls.length)];
80 opponent = challenges[choice];
81 }
82 }
83 else if (challenges[vname]) {
84 opponent = challenges[vname];
85 choice = vname;
86 }
87 if (opponent) {
88 delete challenges[choice];
89 if (choice == "_random") {
90 // Pick a variant at random in the list
91 const index = Math.floor(Math.random() * variants.length);
92 choice = variants[index].name;
93 }
94 // Launch game
95 let players = [
96 {sid: sid, name: obj.name, randvar: randvar},
97 opponent
98 ];
99 if (Math.random() < 0.5) players = players.reverse();
100 launchGame(choice, players, {}); //empty options => default
101 }
102 else
103 // Place challenge and wait. 'randvar' indicate if we play anything
104 challenges[vname] = {sid: sid, name: obj.name, randvar: randvar};
105 break;
106 }
107 // Set FEN after game was created
108 case "setfen":
109 games[obj.gid].fen = obj.fen;
110 break;
111 // Send back game informations
112 case "getgame": {
113 if (!games[obj.gid]) send(sid, "nogame");
114 else send(sid, "gameinfo", games[obj.gid]);
115 break;
116 }
117 // Cancel challenge
118 case "cancelseek":
119 delete challenges[obj.vname];
120 break;
121 // Receive rematch
122 case "rematch": {
123 if (!games[obj.gid]) send(sid, "closerematch");
124 else {
125 const myIndex = (games[obj.gid].players[0].sid == sid ? 0 : 1);
126 if (!games[obj.gid].rematch) games[obj.gid].rematch = [false, false];
127 games[obj.gid].rematch[myIndex] = true;
128 if (games[obj.gid].rematch[1-myIndex]) {
129 // Launch new game, colors reversed
130 launchGame(games[obj.gid].vname,
131 games[obj.gid].players.reverse(),
132 games[obj.gid].options);
133 }
134 }
135 break;
136 }
137 // Rematch cancellation
138 case "norematch": {
139 if (games[obj.gid]) {
140 const myIndex = (games[obj.gid].players[0].sid == sid ? 0 : 1);
141 send(games[obj.gid].players[1-myIndex].sid, "closerematch");
142 }
143 break;
144 }
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 // Join game vs. friend
160 case "joingame": {
161 if (!games[obj.gid]) send(sid, "jointoolate");
162 else {
163 // Join a game (started by some other player)
164 const emptySlot = games[obj.gid].players.findIndex(p => !p);
165 if (emptySlot < 0) send(sid, "jointoolate");
166 games[obj.gid].players[emptySlot] = {sid: sid, name: obj.name};
167 const gameInfo = Object.assign(
168 // Provide seed so that both players initialize with same FEN
169 {seed: Math.floor(Math.random()*1984), gid: obj.gid},
170 games[obj.gid]);
171 for (let i of [0, 1])
172 send(games[obj.gid].players[i].sid, "gamestart", gameInfo);
173 }
174 break;
175 }
176 // Relay a move + update games object
177 case "newmove": {
178 // TODO?: "pingback" strategy to ensure that move was transmitted
179 games[obj.gid].fen = obj.fen;
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 }
185 // Relay "game ends" message
186 case "gameover": {
187 const playingWhite = (games[obj.gid].players[0].sid == sid);
188 const oppSid = games[obj.gid].players[playingWhite ? 1 : 0].sid;
189 if (obj.relay) send(oppSid, "gameover", { gid: obj.gid });
190 games[obj.gid].over = true;
191 setTimeout( () => delete games[obj.gid], 60000 );
192 break;
193 }
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 interval = 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 wss.on('close', () => clearInterval(interval));