forked from fxaa/codenames.plus
-
Notifications
You must be signed in to change notification settings - Fork 0
/
app.js
513 lines (441 loc) · 19.9 KB
/
app.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
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
////////////////////////////////////////////////////////////////////////////
// Express
let express = require('express')
// Create app
let app = express()
//Set up server
let server = app.listen(process.env.PORT || 2000, listen);
// Callback function confirming server start
function listen(){
let host = server.address().address;
let port = server.address().port;
console.log('Codenames Server Started at http://' + host + ':' + port);
}
// Force SSL
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(`https://${req.header('host')}${req.url}`)
} else {
next();
}
});
// Files for client
app.use(express.static('public'))
// Websocket
let io = require('socket.io')(server)
// Catch wildcard socket events
var middleware = require('socketio-wildcard')()
io.use(middleware)
// Make API requests
const Heroku = require('heroku-client')
const heroku = new Heroku({ token:process.env.API_TOKEN})// DELETE requests
// Daily Server Restart time
// UTC 13:00:00 = 9AM EST
let restartHour = 11//13 original
let restartMinute = 0//0
let restartSecond = 5
// restart warning time
let restartWarningHour = 10//12 original
let restartWarningMinute = 50//50
let restartWarningSecond = 2
////////////////////////////////////////////////////////////////////////////
// Codenames Game
const Game = require('./server/game.js')
// Objects to keep track of sockets, rooms and players
let SOCKET_LIST = {}
let ROOM_LIST = {}
let PLAYER_LIST = {}
// Room class
// Live rooms will have a name and password and keep track of game options / players in room
class Room {
constructor(name, pass){
this.room = '' + name
this.password = '' + pass
this.players = {}
this.game = new Game()
this.difficulty = 'normal'
this.mode = 'casual'
// Add room to room list
ROOM_LIST[this.room] = this
}
}
// Player class
// When players log in, they give a nickname, have a socket and a room they're trying to connect to
class Player {
constructor(nickname, room, socket){
this.id = socket.id
// If someone in the room has the same name, append (1) to their nickname
let nameAvailable = false
let nameExists = false;
let tempName = nickname
let counter = 0
while (!nameAvailable){
if (ROOM_LIST[room]){
nameExists = false;
for (let i in ROOM_LIST[room].players){
if (ROOM_LIST[room].players[i].nickname === tempName) nameExists = true
}
if (nameExists) tempName = nickname + "(" + ++counter + ")"
else nameAvailable = true
}
}
this.nickname = tempName
this.room = room
this.team = 'undecided'
this.role = 'guesser'
this.timeout = 2100 // # of seconds until kicked for afk (35min)
this.afktimer = this.timeout
// Add player to player list and add their socket to the socket list
PLAYER_LIST[this.id] = this
}
// When a player joins a room, evenly distribute them to a team
joinTeam(){
let numInRoom = Object.keys(ROOM_LIST[this.room].players).length
if (numInRoom % 2 === 0) this.team = 'blue'
else this.team = 'red'
}
}
// Server logic
////////////////////////////////////////////////////////////////////////////
io.sockets.on('connection', function(socket){
// Alert server of the socket connection
SOCKET_LIST[socket.id] = socket
logStats('CONNECT: ' + socket.id)
// Pass server stats to client
socket.emit('serverStats', {
players: Object.keys(PLAYER_LIST).length,
rooms: Object.keys(ROOM_LIST).length
})
// LOBBY STUFF
////////////////////////////////////////////////////////////////////////////
// Room Creation. Called when client attempts to create a rooom
// Data: player nickname, room name, room password
socket.on('createRoom', (data) => {createRoom(socket, data)})
// Room Joining. Called when client attempts to join a room
// Data: player nickname, room name, room password
socket.on('joinRoom', (data) => {joinRoom(socket, data)})
// Room Leaving. Called when client leaves a room
socket.on('leaveRoom', () =>{leaveRoom(socket)})
// Client Disconnect
socket.on('disconnect', () => {socketDisconnect(socket)})
// GAME STUFF
////////////////////////////////////////////////////////////////////////////
// Join Team. Called when client joins a team (red / blue)
// Data: team color
socket.on('joinTeam', (data) => {
if (!PLAYER_LIST[socket.id]) return // Prevent Crash
let player = PLAYER_LIST[socket.id]; // Get player who made request
player.team = data.team // Update their team
gameUpdate(player.room) // Update the game for everyone in their room
})
// Randomize Team. Called when client randomizes the teams
socket.on('randomizeTeams', () => {randomizeTeams(socket)})
// New Game. Called when client starts a new game
socket.on('newGame', () =>{newGame(socket)})
// Switch Role. Called when client switches to spymaster / guesser
// Data: New role
socket.on('switchRole', (data) => {switchRole(socket, data)})
// Switch Difficulty. Called when spymaster switches to hard / normal
// Data: New difficulty
socket.on('switchDifficulty', (data) => {
if (!PLAYER_LIST[socket.id]) return // Prevent Crash
let room = PLAYER_LIST[socket.id].room // Get room the client was in
ROOM_LIST[room].difficulty = data.difficulty // Update the rooms difficulty
gameUpdate(room) // Update the game for everyone in this room
})
// Switch Mode. Called when client switches to casual / timed
// Data: New mode
socket.on('switchMode', (data) => {
if (!PLAYER_LIST[socket.id]) return // Prevent Crash
let room = PLAYER_LIST[socket.id].room // Get the room the client was in
ROOM_LIST[room].mode = data.mode; // Update the rooms game mode
ROOM_LIST[room].game.timer = ROOM_LIST[room].game.timerAmount; // Reset the timer in the room's game
gameUpdate(room) // Update the game for everyone in this room
})
// End Turn. Called when client ends teams turn
socket.on('endTurn', () => {
if (!PLAYER_LIST[socket.id]) return // Prevent Crash
let room = PLAYER_LIST[socket.id].room // Get the room the client was in
ROOM_LIST[room].game.switchTurn() // Switch the room's game's turn
gameUpdate(room) // Update the game for everyone in this room
})
// Click Tile. Called when client clicks a tile
// Data: x and y location of tile in grid
socket.on('clickTile', (data) => {clickTile(socket, data)})
// Active. Called whenever client interacts with the game, resets afk timer
socket.on('*', () => {
if (!PLAYER_LIST[socket.id]) return // Prevent Crash
PLAYER_LIST[socket.id].afktimer = PLAYER_LIST[socket.id].timeout
})
// Change card packs
socket.on('changeCards', (data) => {
if (!PLAYER_LIST[socket.id]) return // Prevent Crash
let room = PLAYER_LIST[socket.id].room // Get the room the client was in
let game = ROOM_LIST[room].game
if(data.pack === 'base'){ // Toggle packs in the game
game.base = !game.base
} else if (data.pack === 'duet'){
game.duet = !game.duet
} else if (data.pack === 'undercover'){
game.undercover = !game.undercover
} else if (data.pack === 'nlss'){
game.nlss = !game.nlss
}
// If all options are disabled, re-enable the base pack
if (!game.base && !game.duet && !game.undercover && !game.nlss) game.base = true
game.updateWordPool()
gameUpdate(room)
})
// Change timer slider
socket.on('timerSlider', (data) => {
if (!PLAYER_LIST[socket.id]) return // Prevent Crash
let room = PLAYER_LIST[socket.id].room // Get the room the client was in
let game = ROOM_LIST[room].game
let currentAmount = game.timerAmount - 1 // Current timer amount
let seconds = (data.value * 60) + 1 // the new amount of the slider
if (currentAmount !== seconds){ // if they dont line up, update clients
game.timerAmount = seconds
game.timer = game.timerAmount
gameUpdate(room)
}
})
})
// Create room function
// Gets a room name and password and attempts to make a new room if one doesn't exist
// On creation, the client that created the room is created and added to the room
function createRoom(socket, data){
let roomName = data.room.trim() // Trim whitespace from room name
let passName = data.password.trim() // Trim whitespace from password
let userName = data.nickname.trim() // Trim whitespace from nickname
if (ROOM_LIST[roomName]) { // If the requested room name is taken
// Tell the client the room arleady exists
socket.emit('createResponse', {success:false, msg:'Room Already Exists'})
} else {
if (roomName === "") {
// Tell the client they need a valid room name
socket.emit('createResponse', {success:false, msg:'Enter A Valid Room Name'})
} else {
if (userName === ''){
// Tell the client they need a valid nickname
socket.emit('createResponse', {success:false, msg:'Enter A Valid Nickname'})
} else { // If the room name and nickname are both valid, proceed
new Room(roomName, passName) // Create a new room
let player = new Player(userName, roomName, socket) // Create a new player
ROOM_LIST[roomName].players[socket.id] = player // Add player to room
player.joinTeam() // Distribute player to team
socket.emit('createResponse', {success:true, msg: ""})// Tell client creation was successful
gameUpdate(roomName) // Update the game for everyone in this room
logStats(socket.id + "(" + player.nickname + ") CREATED '" + ROOM_LIST[player.room].room + "'(" + Object.keys(ROOM_LIST[player.room].players).length + ")")
}
}
}
}
// Join room function
// Gets a room name and poassword and attempts to join said room
// On joining, the client that joined the room is created and added to the room
function joinRoom(socket, data){
let roomName = data.room.trim() // Trim whitespace from room name
let pass = data.password.trim() // Trim whitespace from password
let userName = data.nickname.trim() // Trim whitespace from nickname
if (!ROOM_LIST[roomName]){
// Tell client the room doesnt exist
socket.emit('joinResponse', {success:false, msg:"Room Not Found"})
} else {
if (ROOM_LIST[roomName].password !== pass){
// Tell client the password is incorrect
socket.emit('joinResponse', {success:false, msg:"Incorrect Password"})
} else {
if (userName === ''){
// Tell client they need a valid nickname
socket.emit('joinResponse', {success:false, msg:'Enter A Valid Nickname'})
} else { // If the room exists and the password / nickname are valid, proceed
let player = new Player(userName, roomName, socket) // Create a new player
ROOM_LIST[roomName].players[socket.id] = player // Add player to room
player.joinTeam() // Distribute player to team
socket.emit('joinResponse', {success:true, msg:""}) // Tell client join was successful
gameUpdate(roomName) // Update the game for everyone in this room
// Server Log
logStats(socket.id + "(" + player.nickname + ") JOINED '" + ROOM_LIST[player.room].room + "'(" + Object.keys(ROOM_LIST[player.room].players).length + ")")
}
}
}
}
// Leave room function
// Gets the client that left the room and removes them from the room's player list
function leaveRoom(socket){
if (!PLAYER_LIST[socket.id]) return // Prevent Crash
let player = PLAYER_LIST[socket.id] // Get the player that made the request
delete PLAYER_LIST[player.id] // Delete the player from the player list
delete ROOM_LIST[player.room].players[player.id] // Remove the player from their room
gameUpdate(player.room) // Update everyone in the room
// Server Log
logStats(socket.id + "(" + player.nickname + ") LEFT '" + ROOM_LIST[player.room].room + "'(" + Object.keys(ROOM_LIST[player.room].players).length + ")")
// If the number of players in the room is 0 at this point, delete the room entirely
if (Object.keys(ROOM_LIST[player.room].players).length === 0) {
delete ROOM_LIST[player.room]
logStats("DELETE ROOM: '" + player.room + "'")
}
socket.emit('leaveResponse', {success:true}) // Tell the client the action was successful
}
// Disconnect function
// Called when a client closes the browser tab
function socketDisconnect(socket){
let player = PLAYER_LIST[socket.id] // Get the player that made the request
delete SOCKET_LIST[socket.id] // Delete the client from the socket list
delete PLAYER_LIST[socket.id] // Delete the player from the player list
if(player){ // If the player was in a room
delete ROOM_LIST[player.room].players[socket.id] // Remove the player from their room
gameUpdate(player.room) // Update everyone in the room
// Server Log
logStats(socket.id + "(" + player.nickname + ") LEFT '" + ROOM_LIST[player.room].room + "'(" + Object.keys(ROOM_LIST[player.room].players).length + ")")
// If the number of players in the room is 0 at this point, delete the room entirely
if (Object.keys(ROOM_LIST[player.room].players).length === 0) {
delete ROOM_LIST[player.room]
logStats("DELETE ROOM: '" + player.room + "'")
}
}
// Server Log
logStats('DISCONNECT: ' + socket.id)
}
// Randomize Teams function
// Will mix up the teams in the room that the client is in
function randomizeTeams(socket){
if (!PLAYER_LIST[socket.id]) return // Prevent Crash
let room = PLAYER_LIST[socket.id].room // Get the room that the client called from
let players = ROOM_LIST[room].players // Get the players in the room
let color = 0; // Get a starting color
if (Math.random() < 0.5) color = 1
let keys = Object.keys(players) // Get a list of players in the room from the dictionary
let placed = [] // Init a temp array to keep track of who has already moved
while (placed.length < keys.length){
let selection = keys[Math.floor(Math.random() * keys.length)] // Select random player index
if (!placed.includes(selection)) placed.push(selection) // If index hasn't moved, move them
}
// Place the players in alternating teams from the new random order
for (let i = 0; i < placed.length; i++){
let player = players[placed[i]]
if (color === 0){
player.team = 'red'
color = 1
} else {
player.team = 'blue'
color = 0
}
}
gameUpdate(room) // Update everyone in the room
}
// New game function
// Gets client that requested the new game and instantiates a new game board for the room
function newGame(socket){
if (!PLAYER_LIST[socket.id]) return // Prevent Crash
let room = PLAYER_LIST[socket.id].room // Get the room that the client called from
ROOM_LIST[room].game.init(); // Make a new game for that room
// Make everyone in the room a guesser and tell their client the game is new
for(let player in ROOM_LIST[room].players){
PLAYER_LIST[player].role = 'guesser';
SOCKET_LIST[player].emit('switchRoleResponse', {success:true, role:'guesser'})
SOCKET_LIST[player].emit('newGameResponse', {success:true})
}
gameUpdate(room) // Update everyone in the room
}
// Switch role function
// Gets clients requested role and switches it
function switchRole(socket, data){
if (!PLAYER_LIST[socket.id]) return // Prevent Crash
let room = PLAYER_LIST[socket.id].room // Get the room that the client called from
if (PLAYER_LIST[socket.id].team === 'undecided'){
// Dissallow the client a role switch if they're not on a team
socket.emit('switchRoleResponse', {success:false})
} else {
PLAYER_LIST[socket.id].role = data.role; // Set the new role
socket.emit('switchRoleResponse', {success:true, role:data.role}) // Alert client
gameUpdate(room) // Update everyone in the room
}
}
// Click tile function
// Gets client and the tile they clicked and pushes that change to the rooms game
function clickTile(socket, data){
if (!PLAYER_LIST[socket.id]) return // Prevent Crash
let room = PLAYER_LIST[socket.id].room // Get the room that the client called from
if (PLAYER_LIST[socket.id].team === ROOM_LIST[room].game.turn){ // If it was this players turn
if (!ROOM_LIST[room].game.over){ // If the game is not over
if (PLAYER_LIST[socket.id].role !== 'spymaster'){ // If the client isnt spymaster
ROOM_LIST[room].game.flipTile(data.i, data.j) // Send the flipped tile info to the game
gameUpdate(room) // Update everyone in the room
}
}
}
}
// Update the gamestate for every client in the room that is passed to this function
function gameUpdate(room){
// Create data package to send to the client
let gameState = {
room: room,
players:ROOM_LIST[room].players,
game:ROOM_LIST[room].game,
difficulty:ROOM_LIST[room].difficulty,
mode:ROOM_LIST[room].mode
}
for (let player in ROOM_LIST[room].players){ // For everyone in the passed room
gameState.team = PLAYER_LIST[player].team // Add specific clients team info
SOCKET_LIST[player].emit('gameState', gameState) // Pass data to the client
}
}
function logStats(addition){
let inLobby = Object.keys(SOCKET_LIST).length - Object.keys(PLAYER_LIST).length
let stats = '[R:' + Object.keys(ROOM_LIST).length + " P:" + Object.keys(PLAYER_LIST).length + " L:" + inLobby + "] "
console.log(stats + addition)
}
// Restart Heroku Server
function herokuRestart(){
// Let each socket know the server restarted and boot them to lobby
for (let socket in SOCKET_LIST){
SOCKET_LIST[socket].emit('serverMessage', {msg:"Server Successfully Restarted for Maintnence"})
SOCKET_LIST[socket].emit('leaveResponse', {success:true})
}
heroku.delete('/apps/codenames-plus/dynos/').then(app => {})
}
// Warn users of restart
function herokuRestartWarning(){
for (let player in PLAYER_LIST){
SOCKET_LIST[player].emit('serverMessage', {msg:"Scheduled Server Restart in 10 Minutes"})
}
}
// Every second, update the timer in the rooms that are on timed mode
setInterval(()=>{
// Server Daily Restart Logic
let time = new Date()
// Warn clients of restart 10min in advance
if (time.getHours() === restartWarningHour &&
time.getMinutes() === restartWarningMinute &&
time.getSeconds() < restartWarningSecond) herokuRestartWarning()
// Restart server at specified time
if (time.getHours() === restartHour &&
time.getMinutes() === restartMinute &&
time.getSeconds() < restartSecond) herokuRestart()
// AFK Logic
for (let player in PLAYER_LIST){
PLAYER_LIST[player].afktimer-- // Count down every players afk timer
// Give them a warning 5min before they get kicked
if (PLAYER_LIST[player].afktimer < 300) SOCKET_LIST[player].emit('afkWarning')
if (PLAYER_LIST[player].afktimer < 0) { // Kick player if their timer runs out
SOCKET_LIST[player].emit('afkKicked')
logStats(player + "(" + PLAYER_LIST[player].nickname + ") AFK KICKED FROM '" + ROOM_LIST[PLAYER_LIST[player].room].room + "'(" + Object.keys(ROOM_LIST[PLAYER_LIST[player].room].players).length + ")")
leaveRoom(SOCKET_LIST[player])
}
}
// Game Timer Logic
for (let room in ROOM_LIST){
if (ROOM_LIST[room].mode === 'timed'){
ROOM_LIST[room].game.timer-- // If the room is in timed mode, count timer down
if (ROOM_LIST[room].game.timer < 0){ // If timer runs out, switch that rooms turn
ROOM_LIST[room].game.switchTurn()
gameUpdate(room) // Update everyone in the room
}
// Update the timer value to every client in the room
for (let player in ROOM_LIST[room].players){
SOCKET_LIST[player].emit('timerUpdate', {timer:ROOM_LIST[room].game.timer})
}
}
}
}, 1000)