-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.js
347 lines (312 loc) · 12.4 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
const websockets = require('ws');
const Board = require("./board.js");
const printDebugLevelMsgs = true;
const wsPort = process.env.PORT || 8000;
const oldBoardTimeout = 20 /* mins */ * 60 /* secs */ * 1000 /* ms */ ;
const timeBetweenOldBoardCleanupPasses = 1 /* mins */ * 60 /* secs */ * 1000 /* ms */ ;
const timeBetweenHeartbeatMsgs = 1 /* mins */ * 60 /* secs */ * 1000 /* ms */ ;
// Log config messages to the terminal.
console.log("CONFIG: WS server on port:\t" + wsPort);
console.log("CONFIG: Time formatted in MM/DD/YYYY HH:MM::SS:MS, local server time zone");
console.log("CONFIG: Server Timezone:\tUTC" + (-(new Date).getTimezoneOffset() / 60));
console.log();
console.log("BEGIN SERVER LOG OUTPUT");
// {Board, Board, Board, ...}
var boardsList = [];
boardsList.remove = function (index) {
// https://stackoverflow.com/a/53069926/3339274
var numToRemove = 1;
this.splice(index, numToRemove);
}
// Log a message to the terminal.
// critical (bool): Is the message critical or debug?
// msg (string): Message to print.
function logMsg(critical, msg) {
var date = new Date(Date.now());
var stringDate = date.getMonth() + "/" + date.getDate() + "/" + date.getFullYear() +
" " + date.getHours() + ":" + date.getMinutes() + ":" +
date.getSeconds() + ":" + date.getMilliseconds();
if (critical) {
console.log(stringDate + " -> CRITICAL ERR: " + msg);
} else if (printDebugLevelMsgs) {
console.log(stringDate + " -> DEBUG: " + msg);
}
}
// List of issued session IDs which will be accepted by the WebSockets API.
var issuedSIDs = [];
// Creates a random string of characters length characters long.
function makeSessionID(length) {
logMsg(false, "Session ID of length " + length + " created");
// Make new session IDs until we find one that isn't already in use.
var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var newSessionID;
var viable = true;
do {
newSessionID = "";
for (var i = 0; i < length; i++)
newSessionID += characters.charAt(Math.floor(Math.random() * characters.length));
// Is the generated sessionID in use?
for (var i = 0; i < issuedSIDs.length; i++)
if (newSessionID === issuedSIDs[i]) viable = false;
} while (!viable)
// Push the new session ID onto the list of issued IDs.
issuedSIDs.push(newSessionID);
return newSessionID;
}
// Send board update to all players in the board.
function sendBoardUpdate(board) {
logMsg(false, "Sending board object updates to all players");
for (var i = 0; i < board.getPlayers().length; i++) {
// logMsg(false, "Sending to sessionID: " + board.getPlayers()[i].sID)
board.getPlayers()[i].connection.send(JSON.stringify({
msgType: "boardUpdate",
board: board.getAsArray()
}));
}
}
// Find the board that this player is on.
// ws (websocket connection): WebSocket to use for a possible error message.
// sessionID (string): sessionID of the query user.
function findPlayersBoard(ws, sessionID) {
var board = null;
for (var i = 0; i < boardsList.length; i++) {
var playersList = boardsList[i].getPlayers();
for (var j = 0; j < playersList.length; j++) {
if (playersList[j].sID === sessionID) {
board = boardsList[i];
break;
}
}
}
// IF no board was found, error out.
if (board == null) {
ws.send(JSON.stringify({
msgType: "ERR",
err: "Board not found"
}));
return null;
} else return board;
}
// Helper function called on a cycle timer which removes old and unused boards
// from the list.
function cleanUpOldBoards() {
logMsg(false, "Cleaning up old boards.");
var numBoardsRemoved = 0;
for (var i = 0; i < boardsList.length; i++) {
var creationTime = boardsList[i].getWhenGameBegan().getTime();
// If the board hasn't started yet, then don't delete it, you buffoon!
if (creationTime == 0) continue;
// If the board has been around longer than it's allowed to, clean it up.
if ((new Date()).getTime() - creationTime > oldBoardTimeout) {
// Send the cleanup message to all players.
for (var j = 0; j < boardsList[i].getPlayers().length; j++) {
logMsg(false, "Sending board cleanup to sessionID: " + boardsList[i].getPlayers()[j].sID)
boardsList[i].getPlayers()[j].connection.send(JSON.stringify({
msgType: "gameTimeout",
}));
}
// Remove the board from the list.
boardsList.remove(i);
numBoardsRemoved++;
}
}
if (numBoardsRemoved > 0) logMsg(false, "Cleanup routine removed " +
numBoardsRemoved + " empty/unused boards");
else logMsg(false, "Cleanup routine removed no boards");
}
// Helper function called on a cycle timer to keep the websocket connection alive.
function sendHeartbeatMsgs() {
for (var i = 0; i < boardsList.length; i++) {
var playersList = boardsList[i].getPlayers();
for (var j = 0; j < playersList.length; j++) {
playersList[j].connection.send(JSON.stringify({
msgType: "heartbeat"
}));
}
}
}
// Handles an incoming WebSockets message.
// ws (websocket connection): WebSocket to use for communication with the current client.
// msg (JSON string): Non-parsed JSON string with the message body.
function handleWsMessage(ws, msg) {
// Attempt to parse the JSON, catching possible exceptions.
var parsedMsg;
try {
parsedMsg = JSON.parse(msg);
} catch (SyntaxError) {
logMsg(false, "ERROR!: Malformed JSON message received.");
ws.send(JSON.stringify({
msgType: "ERR",
err: "Malformed JSON"
}));
return;
}
// Is the provided session ID in the list of issued IDs?
if (parsedMsg.msgType !== "signup") { // signup messages won't contain sIDs.
var validSID = false;
for (var i = 0; i < issuedSIDs.length; i++) {
if (parsedMsg.sessionID === issuedSIDs[i]) {
validSID = true;
break;
}
}
if (!validSID) {
logMsg(true, "ERR!: Session ID " + parsedMsg.sessionID + " is invalid!");
ws.send(JSON.stringify({
msgType: "ERR",
err: "SessionID isn't valid"
}));
return;
}
}
// Handle the request:
switch (parsedMsg.msgType) {
case "signup":
logMsg(false, "Signup message received. Name: " + parsedMsg.name);
// Look for boards which have less than a full set of players.
var freeBoard = -1;
for (var i = 0; i < boardsList.length; i++) {
if (!boardsList[i].isFull()) freeBoard = i;
}
// If there were no free boards, create one.
if (freeBoard == -1) {
boardsList.push(new Board());
freeBoard = boardsList.length - 1;
}
// Add the player to the board.
var newSessionID = makeSessionID(8);
boardsList[freeBoard].addPlayer(newSessionID, ws, parsedMsg.name);
logMsg(false, "New player added to board " + freeBoard + " sID " +
newSessionID);
// Send the client its new session ID.
ws.send(JSON.stringify({
msgType: "signupSuccess",
sessionId: newSessionID
}));
// If this made the board full, then start the game.
if (boardsList[freeBoard].isFull()) {
// Initialize the board, so that it has boxes.
boardsList[freeBoard].initBoard();
// Tell all players that the game is starting and send the first board object.
var playersList = boardsList[freeBoard].getPlayers();
for (var i = 0; i < playersList.length; i++) {
logMsg(false, "Sending game starting message to board: " + freeBoard + ", player: " + i)
playersList[i].connection.send(JSON.stringify({
msgType: "gameStarting",
playerId: i
}));
playersList[i].connection.send(JSON.stringify({
msgType: "boardUpdate",
board: boardsList[freeBoard].getAsArray()
}));
}
// Else, tell all players that the game how many players still need to join.
} else {
logMsg(false, "Board " + freeBoard + " still isn't full, just sending waiting message.")
var playersList = boardsList[freeBoard].getPlayers();
var numLeft = 4 - playersList.length;
for (var i = 0; i < playersList.length; i++) {
playersList[i].connection.send(JSON.stringify({
msgType: "waitingForPlayers",
numLeft: numLeft
}));
}
}
break;
case "clientClosed":
logMsg(false, "Client closed message received. sID: " + parsedMsg.sessionID);
// Find the board that this player is on.
var sessionID = parsedMsg.sessionID;
var board = findPlayersBoard(ws, sessionID);
// If the player's board was found, delete the gosh darn player!
if (board == null) {
logMsg(true, "Error!: Board not found for sessionID: " +
parsedMsg.sessionID);
} else {
// Remove the player from the game.
logMsg(false, "OLD length: " + board.getPlayers().length);
board.removePlayer(sessionID);
logMsg(false, "NEW length: " + board.getPlayers().length);
// Tell the other clients of the change.
var playersList = board.getPlayers();
for (var i = 0; i < playersList.length; i++) {
logMsg(false, "Player with sID: " + sessionID + " removed from their board. Notifying client with sID: " + playersList[i].sID);
playersList[i].connection.send(JSON.stringify({
msgType: "waitingForPlayers",
numLeft: 4 - playersList.length
}));
}
}
break;
case "playerMove":
logMsg(false, "Player move message received. sID: " + parsedMsg.sessionID +
", Direction: " + parsedMsg.direction);
// Find the board that this player is on.
var sessionID = parsedMsg.sessionID;
var board = findPlayersBoard(ws, sessionID);
// If the player's board was found, handle the gosh darn box!
if (board == null) {
logMsg(true, "Error!: Board not found for sessionID: " +
parsedMsg.sessionID);
} else {
var player;
for (var i = 0; i < board.getPlayers().length; i++) {
if (board.getPlayers()[i].sID == sessionID) {
player = i + 1;
break;
}
}
if (player == null) {
logMsg(true, "Error!: Player not found in board for sessionID: " +
parsedMsg.sessionID);
} else {
board.handleBoardMove(parsedMsg.direction, player);
sendBoardUpdate(board);
}
}
break;
case "unlockBox":
logMsg(false, "Box unlock message received. sID: " + parsedMsg.sessionID +
", row: " + parsedMsg.row + ", col: " + parsedMsg.col);
// Find the board that this player is on.
var sessionID = parsedMsg.sessionID;
var board = findPlayersBoard(ws, sessionID);
// If the player's board was found, enable the gosh darn box!
if (board == null) {
logMsg(true, "Error!: Board not found for sessionID: " +
parsedMsg.sessionID)
} else {
board.enableBox(parsedMsg.row, parsedMsg.col);
sendBoardUpdate(board);
}
break;
default:
logMsg(true, "Error!: Unrecognized received. Message: " + parsedMsg);
ws.send(JSON.stringify({
msgType: "ERR",
err: "Invalid message type"
}));
break;
}
}
// *****************************************************************************
// WebSockets Server init and setup.
// *****************************************************************************
// Setup periodic cleanup of old boards.
// https://stackoverflow.com/a/1224485/3339274
var cleanupHandle = setInterval(cleanUpOldBoards, timeBetweenOldBoardCleanupPasses);
//clearInterval(cleanupHandle)
// Setup periodic heartbeat message to all players.
// https://stackoverflow.com/a/1224485/3339274
var heartbeatHandle = setInterval(sendHeartbeatMsgs, timeBetweenHeartbeatMsgs);
//clearInterval(heartbeatHandle)
// Start WebSocket Server to listen for new players making moves/actions.
new websockets.Server({
port: wsPort
}).on('connection', function (socket) {
logMsg(false, "Connection established with new client");
socket.on('message', function incoming(msg) {
logMsg(false, "WebSocket message received: " + msg);
handleWsMessage(socket, msg);
});
});