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