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
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;
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:
20 socket
.send(JSON
.stringify(Object
.assign({code: code
}, data
)));
23 function initializeGame(vname
, players
, options
) {
25 Crypto
.randomBytes(randstrSize
).toString("hex").slice(0, randstrSize
);
31 moveHash: {} //set of moves hashes seen so far
36 // Provide seed in case of, so that both players initialize with same FEN
37 function launchGame(gid
) {
38 const gameInfo
= Object
.assign(
39 {seed: Math
.floor(Math
.random() * 19840), gid: gid
},
42 // players array is supposed to be full:
43 for (const p
of games
[gid
].players
)
44 send(p
.sid
, "gamestart", gameInfo
);
47 function getRandomVariant() {
48 // Pick a variant at random in the list
49 const index
= Math
.floor(Math
.random() * variants
.length
);
50 return variants
[index
].name
;
53 wss
.on("connection", (socket
, req
) => {
54 const sid
= req
.url
.split("=")[1]; //...?sid=...
55 sockets
[sid
] = socket
;
56 socket
.isAlive
= true;
57 socket
.on("pong", () => socket
.isAlive
= true);
58 if (params
.dev
== true) {
59 const chokidar
= require("chokidar");
60 const watcher
= chokidar
.watch(
61 ["*.js", "*.css", "utils/", "variants/"],
63 watcher
.on("change", path
=> send(sid
, "filechange", {path: path
}));
65 socket
.on("message", (msg
) => {
66 const obj
= JSON
.parse(msg
);
68 // Send challenge (may trigger game creation)
70 let oppIndex
= undefined, //variant name
71 choice
= undefined; //variant finally played
72 const vname
= obj
.vname
, //variant requested
73 randvar
= (obj
.vname
== "_random");
74 if (vname
== "_random") {
75 // Pick any current challenge if possible
76 const currentChalls
= Object
.keys(challenges
);
77 if (currentChalls
.length
>= 1) {
79 currentChalls
[Math
.floor(Math
.random() * currentChalls
.length
)];
83 else if (challenges
[vname
]) {
84 // Anyone wanting to play the same variant ?
88 else if (challenges
["_random"]) {
89 // Anyone accepting any variant (including vname) ?
94 if (choice
== "_random")
95 choice
= getRandomVariant();
98 {sid: sid
, name: obj
.name
, randvar: randvar
},
99 Object
.assign({}, challenges
[oppIndex
])
101 delete challenges
[oppIndex
];
102 if (Math
.random() < 0.5)
103 players
= players
.reverse();
104 // Empty options = default
105 launchGame( initializeGame(choice
, players
, {}) );
108 // Place challenge and wait. 'randvar' indicate if we play anything
109 challenges
[vname
] = {sid: sid
, name: obj
.name
, randvar: randvar
};
112 // Set FEN after game was created (received twice)
114 games
[obj
.gid
].fen
= obj
.fen
;
116 // Send back game informations
121 send(sid
, "gameinfo", games
[obj
.gid
]);
125 delete challenges
[obj
.vname
];
130 send(sid
, "closerematch");
132 const myIndex
= (games
[obj
.gid
].players
[0].sid
== sid
? 0 : 1);
133 if (!games
[obj
.gid
].rematch
)
134 games
[obj
.gid
].rematch
= [0, 0];
135 games
[obj
.gid
].rematch
[myIndex
] = !obj
.random
? 1 : 2;
136 if (games
[obj
.gid
].rematch
[1-myIndex
]) {
137 // Launch new game, colors reversed
138 let vname
= games
[obj
.gid
].vname
;
139 const allrand
= games
[obj
.gid
].rematch
.every(r
=> r
== 2);
141 vname
= getRandomVariant();
142 games
[obj
.gid
].players
.forEach(p
=> p
.randvar
= allrand
);
143 const gid
= initializeGame(vname
,
144 games
[obj
.gid
].players
.reverse(),
145 games
[obj
.gid
].options
);
150 // Rematch cancellation
152 if (games
[obj
.gid
]) {
153 const myIndex
= (games
[obj
.gid
].players
[0].sid
== sid
? 0 : 1);
154 send(games
[obj
.gid
].players
[1-myIndex
].sid
, "closerematch");
157 // Create game vs. friend
160 {sid: obj
.player
.sid
, name: obj
.player
.name
},
164 obj
.player
.color
== 'b' ||
165 (obj
.player
.color
== '' && Math
.random() < 0.5)
167 players
= players
.reverse();
169 // Incomplete players array: do not start game yet
170 const gid
= initializeGame(obj
.vname
, players
, obj
.options
);
171 send(sid
, "gamecreated", {gid: gid
});
172 // If nobody joins within 3 minutes, delete game
175 if (games
[gid
] && games
[gid
].players
.some(p
=> !p
))
182 // Join game vs. friend
185 send(sid
, "jointoolate");
187 const emptySlot
= games
[obj
.gid
].players
.findIndex(p
=> !p
);
189 send(sid
, "jointoolate");
191 // Join a game (started by some other player)
192 games
[obj
.gid
].players
[emptySlot
] = {sid: sid
, name: obj
.name
};
197 // Relay a move + update games object
199 // NOTE: still potential racing issues, but... fingers crossed
200 const hash
= Crypto
.createHash("md5")
201 .update(JSON
.stringify(obj
.fen
))
203 if (games
[obj
.gid
].moveHash
[hash
])
205 games
[obj
.gid
].moveHash
[hash
] = true;
206 games
[obj
.gid
].fen
= obj
.fen
;
207 games
[obj
.gid
].time
= Date
.now(); //update useful if verrry slow game
208 const playingWhite
= (games
[obj
.gid
].players
[0].sid
== sid
);
209 const oppSid
= games
[obj
.gid
].players
[playingWhite
? 1 : 0].sid
;
210 send(oppSid
, "newmove", {moves: obj
.moves
});
212 // Relay "game ends" message
215 const playingWhite
= (games
[obj
.gid
].players
[0].sid
== sid
);
216 const oppSid
= games
[obj
.gid
].players
[playingWhite
? 1 : 0].sid
;
217 send(oppSid
, "gameover", { gid: obj
.gid
});
219 // 2 minutes timeout for rematch:
220 setTimeout(() => delete games
[obj
.gid
], 2 * 60000);
224 socket
.on("close", () => {
226 for (const [key
, value
] of Object
.entries(challenges
)) {
227 if (value
.sid
== sid
) {
228 delete challenges
[key
];
229 break; //only one challenge per player
232 for (let g
of Object
.values(games
)) {
233 const myIndex
= g
.players
.findIndex(p
=> p
&& p
.sid
== sid
);
235 if (g
.rematch
&& g
.rematch
[myIndex
] > 0) g
.rematch
[myIndex
] = 0;
236 break; //only one game per player
242 const heartbeat
= setInterval(() => {
243 wss
.clients
.forEach((ws
) => {
244 if (ws
.isAlive
=== false)
245 return ws
.terminate();
251 // Every 24 hours, scan games and remove if last move older than 24h
252 const dayInMillisecs
= 24 * 60 * 60 * 1000;
253 const killOldGames
= setInterval(() => {
254 const now
= Date
.now();
255 Object
.keys(games
).forEach(gid
=> {
256 if (now
- games
[gid
].time
>= dayInMillisecs
)
261 // TODO: useful code here?
262 wss
.on("close", () => {
263 clearInterval(heartbeat
);
264 clearInterval(killOldGames
);