forked from VirPong/paddle-meister
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathserver.js
624 lines (547 loc) · 19.2 KB
/
server.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
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
/* server.js v.0.4 for vir-pong, inc */
/* daniel guilak -- [email protected] */
var PORT = 3000; //Require the express framework (which creates a server)
var app = require('express').createServer(),
sys = require(process.binding('natives').util ? 'util' : 'sys')
//socket.io provides websocket support.
sio = require('socket.io');
//set the server to listen on port
app.listen(PORT);
//set the sockets to listen on same port.
var io = sio.listen(app);
io.set('log level', 1); // reduce logging
//Using mongojs to connect to the replays collection database
var rDB = require('mongojs').connect('games',['replays']);
//For connecting to the user database -- db-mysql
//Remember, mysql_client needs to be installed,
//ubuntu package libmysqlclient16-dev will do it!
var mysql = require('db-mysql');
var gClients = []; //The socket.io clients that are connected.
var gRooms = []; //Array of global Room objects.
var gRoomNames = []; //Array of global Room names.
var gNumRooms = 0; //Keeps track of number of rooms (AArrays don't have length).
var MAXSCORE = 1; //Play to this many points.
var UPDATE_INTERVAL = 50; //Number of milliseconds to send updates.
/*
*
* Gets called whenever someone connects to the server.
*
* @param eventName='connection', function(aClient)
* aClient is the reference to the client's socket.io socket.
*
*/
io.sockets.on('connection', function (aClient) {
var newClient;
var isAuthenticated = false;
console.log("Client connecting.");
/*
*
* On authentication request from client.
*
* Authenticates using mySQL database and notifies client of access
* granted or denied.
*
* @param eventName='auth', function(data)
* data.username should have username
* data.password should have password information.
*
* @emits 'authGranted' on successful authentication
* @emits 'authFailed' on unsucessful authentication.
*
*/
aClient.on('auth', function(data){
console.log("Authenticating: " + data.username + " " + data.password);
authenticate(data.username, data.password, function(authenticated){
if(authenticated == true){
aClient.emit('authGranted');
isAuthenticated = true;
newClient = new Client(aClient, data.username); //Creates a new Client.
gClients.push(newClient);
//Sends out an updated room list to the client.
aClient.emit('roomList', {rooms: gRoomNames, numRooms: gNumRooms});
} else {
//if authentication fails:
aClient.emit('authFailed');
console.log("Authentication failed for " + data.username);
}
});
});
/*
*
* On joinRoom from client
* Adds client to a certain Room object, as well as connects them to
* their respective socket.io room for communication purposes.
*
* @param eventName='joinRoom', function(aClient)
* @param aClient is the reference to the client's socket.io socket.
* data.name is the name of the room client wishes to join.
*
*/
aClient.on('joinRoom', function(data){
if(isAuthenticated || data.name == null){ //As long as the client has logged in.
aClient.join(data.name); //Joining socket.io 'room'
newClient.currentRoom = gRooms[data.name]; //Changing current room
newClient.clientType = data.clientType; //Changing type
gRooms[data.name].joinRoom(newClient); //Joining room.
}
});
/*
*
* On createRoom from client
* Creates a new room with specified name, adds client to that Room.
* Also adds client to socket.io group.
*
* @param eventName='createRoom', function(aClient)
* @param aClient is the reference to the client's socket.io socket.
* data.name is the requested name for the room.
*
*/
aClient.on('createRoom', function(data){
if(isAuthenticated){
//Creates the new room and sets its name
var newRoom = new Room(data.name);
//Automatically makes the client a player.
newClient.clientType = 'player';
//Adds the client to the room
addRoom(newRoom);
aClient.join(data.name);
console.log("Room length now: " + gNumRooms);
newRoom.joinRoom(newClient);
newClient.currentRoom = newRoom;
gRoomNames.push(newRoom.name);
}
});
/*
*
* On clientType from client.
* This is how the client specifies if they are spectating or playing.
*
* @param eventName='clientType', function(aClient)
* @param aClient is the reference to the client's socket.io socket.
* aData.type the type of client requested ('player', 'spectator')
*
*/
aClient.on('clientType', function(aData) {
if(isAuthenticated){
newClient.clientType = aData.type;
}
});
/*
*
* On paddleUpdate from client
* As the paddle information for each client is stored within their instance
* of Client, this function catches any paddle update information from a
* particular client and updates their position.
*
* @param eventName='paddleUpdate', function(aClient)
* @param aClient is the reference to the client's socket.io socket.
* aData.pos is a number between 0 and 100 corresponding to what the vertical
* position of a certain paddle is.
*/
aClient.on('paddleUpdate', function(aData) {
if(isAuthenticated){
//update the value of particular paddle position.
newClient.paddlePos = aData.pos;
}
});
/*
*
* On disconnect from client -- gets called when a client
* disconnects.
*
* @param eventName='disconnect', function(aClient)
* @param aClient is the reference to the client's socket.io socket.
*
*/
aClient.on('disconnect', function(data){
//Deletes Client instance from client array
delete gClients[gClients.indexOf(newClient)];
delete newClient;
});
});
/*
*
* Helper function for adding a room to the global room
* lists.
*
* @param newRoom the room to add to the lists.
*/
function addRoom(newRoom) {
gRooms[newRoom.name] = newRoom;
gNumRooms = gNumRooms + 1;
}
/*
*
* Provides functionality for deleting a room after a game ends.
*
* @param r the room instance's name to remove from the server
*
*/
function deleteRoom(r){
console.log("Room is " + r);
var toBeDeleted = gRoomNames.indexOf(r);
console.log("To be deleted: " + toBeDeleted + " " + r);
delete gRoomNames[toBeDeleted];
console.log(gRoomNames);
console.log(gRoomNames[0]);
console.log("Removing " + r);
delete gRooms[r];
gNumRooms = gNumRooms - 1;
//Emit an updated room list to everyone.
io.sockets.emit('roomList', {rooms: gRoomNames, numRooms: gNumRooms});
}
/*
*
* Provides functionality for connecting to the WebUI team's
* mySQL database for authenticating users.
*
* @param user username
* @param pass password
* @param callback(true) if authenticated, callback(false) if not.
*
*/
function authenticate(user, pass, callback){
//SQL Database information -- from Web team.
new mysql.Database({
hostname: 'localhost',
user: 'root',
password: 'sawinrocks',
database: 'db2'
}).connect(function(error) {
if (error) {
console.log('CONNECTION error: ' + error);
}
//Query provided by WebUI team.
this.query('SELECT * FROM Customer WHERE username = \'' + user + '\' AND (password = \'' + pass + '\' OR pin = \'' + pass + '\')').
/*
* Internal method from db library.
*/
execute(function(error, rows, cols) {
if (error) {
console.log('ERROR: ' + error);
callback(false);
} else {
//Authenticates successfuly if a row is returned.
if(rows.length != 0){
callback(true);
} else {
callback(false);
}
}
});
});
}
/*
* Adds a finished game to the WebUI's mySQL database.
*
* @param user1 Player 1's name
* @param user2 Player 2's name.
* @param score1 Player 1's score.
* @param score2 Player 2's score.
* @param winner Winner's name.
*
* Will log an error if it doesn't work.
*/
function addGameDataToSQLDB(user1, user2, score1, score2, winner){
new mysql.Database({
hostname: 'localhost',
user: 'root',
password: 'sawinrocks',
database: 'db2'
}).connect(function(error){
if (error) {
return console.log('CONNECTION error: ' + error);
}
this.query().insert('GamesPlayed',
['username1', 'username2', 'score1', 'score2', 'win'],
[user1, user2, score1, score2, winner]
).execute(function(error, result) {
if (error) {
console.log('ERROR: ' + error);
return;
}
console.log('GENERATED id: ' + result.id);
});
});
}
/*
* Client class provides a way to organize client information.
*
* @field socket the socket.io reference to the client.
* @field name the client's username.
* @field clientType spectator, player, or null (in lobby).
* @field currentRoom pointer to the current room the player is occupying.
* @field paddlePos player's paddle position (if playing)
* @field playerNum player's paddle number (if playing)
*/
function Client (socket, name) {
this.socket = socket;
this.name = name;
this.clientType;
this.currentRoom;
this.paddlePos;
this.playerNum;
}
/**
* Sets a client's room to null, effectively leaving the room.
*
*/
Client.prototype.leaveRoom = function() {
this.currentRoom = null;
}
/**
* Room class, provides a means for organizing each individal game, so that
* multiple games can be played at once! Sadly, the name acts as an ID, but
* it works, so whatever.
*
* @param name the requested name for the room.
*
*/
function Room(name) {
this.name = name;
console.log("this.name is " + this.name);
/* Variable declarations */
this.spectators = new Array();// Spectators
this.players = new Array(); // Players
this.numPlayers = 0; // Number of players currently connected.
this.gameOn = false; // Whether or not the game is being played
/* Game-related variables */
this.ballPos = []; // [ballX, ballY] ball positions.
this.ballV = []; // [ballVX, ballVY] ball velocities.
this.ballR; // The ball radius
this.score = []; // [scorePlayer1, scorePlayer2] player scores.
this.fieldSize = []; // [fieldX, fieldY] size of the game field.
this.paddleSize; // [paddleHeight, paddleWidth]
/* For mongoDB replay functionality */
this.rGameID; //the gameID that will be queried on replays
this.rIndex = 0; //to track replay docs
this.rDocs = []; //an array of replay docs
}
/**
* Preps the room for deletion by forcing all players to leave, and
* removing them from any related arrays.
*
*/
Room.prototype.prepForDeletion = function(name, players){
// for(p in players){
// p.leaveRoom();
//
// //leave from socket.io room.
// p.socket.leave("/"+name);
// }
console.log("trying to delete " + name);
deleteRoom(name);
}
/**
* Function called to add player to room.
*
* @param Client object to add.
*
*/
Room.prototype.joinRoom = function(aClient){
//Sets 'self' reference.
var self = this;
//If the client is a player, give him a paddle ID (0=left, 1=right)
if(aClient.clientType == 'player'){
aClient.socket.emit('paddleID', {paddleID: this.players.length});
console.log("Player " + (this.players.length + 1) + " joined " + "(" + this.name + ")");
//Add him to the players array.
this.players.push(aClient);
}
//If the client is a spectator, just put him on the spectators array.
else if(client.clientType == 'spectator'){
this.spectators.push(client);
}
//If the room has two players, and the game hasn't been started,
if(this.players.length == 2 && !this.gameOn){
//Initialize the game, and start the game with a callback that
//sets it for deletion when the game has completed.
this.startGame(this.prepForDeletion);
//If the game has already started and there are two players,
} else if (this.players.length == 2 && this.gameOn){
//Give the client the gameInfo so that they can have the names of the players.
aClient.emit('gameInfo', {names: [this.players[0].name, this.players[1].name]});
}
}
/**
* The main game loop, uses setInterval to update ball logic and send out
* game state packets and update the mongo replay database.
* @param callback
*
*/
Room.prototype.startGame = function(cb){
//Sets self reference.
var self = this;
//Tells players who they're playing!
io.sockets.in(this.name).emit('gameInfo', {names: [this.players[0].name, this.players[1].name]});
/* Initializing game state variables. */
this.ballPos = [50,50];
this.score = [0,0];
this.fieldSize = [100,100];
this.ballV = [1,2];
this.ballR = (1/20)*this.fieldSize[1];
this.paddleSize = [3,(1/5)*this.fieldSize[1]];
this.players[0].paddlePos = 50;
this.players[1].paddlePos = 50;
//Sets callback function.
var callback = cb;
self.gameOn = true;
self.genGameID(); //generating gameID
//The main game loop.
var gameInterval = setInterval(function() {
self.ballLogic(); //Run ball logic simulation.
self.sendGameState(); //Send game state to all sockets.
//When the game ends, clear the interval (essentially exiting the loop)
//and call the callback function. Runs at UPDATE_INTERVAL ms.
if(self.gameOn == false){
clearInterval(gameInterval);
callback(self.name, self.players);
}
}, UPDATE_INTERVAL);
}
/**
* Sends the Room's current updated game state to all pertinent connected
* clients (players and spectators).
*
* Also adds information to array on its way to mongoDB replay database.
*
*/
Room.prototype.sendGameState = function(){
//Temporary array to simplify code.
var paddles = [this.players[0].paddlePos, this.players[1].paddlePos];
//For every client connected and present in the room, emit a volatile (no ACK required)
//event with the paddle position data and the ball position data.
io.sockets.in(this.name).volatile.emit('gameState', {paddle: paddles, ball: this.ballPos});
//Add the index, paddle position data, ball position data, and socre data to the mongo array, and
//increment the index.
this.rDocs.push({index: this.rIndex, paddle: paddles,
ball: [this.ballPos[0], this.ballPos[1]], scores: [this.score[0], this.score[1]]});
this.rIndex = this.rIndex + 1; //increment the index
}
/**
* Sends the room's current score to whoever is present in the room,
* whether it be players or spectators. Also deals with end-game scenario.
*
*/
Room.prototype.sendScore = function(){
//Set a self reference.
var self = this;
//Emit a scoreUpdate event to all connected clients with new score.
io.sockets.in(this.name).emit('scoreUpdate', { score: this.score });
//If the game is over, set a temporary "winner" variable.
if(this.score[0] == MAXSCORE || this.score[1] == MAXSCORE){
var winner;
if(this.score[0] == MAXSCORE){
winner = this.players[0].name;
}
else if(this.score[1] == MAXSCORE){
winner = this.players[1].name;
}
//Send a gameEnd event to everyone.
console.log("Game " + this.name + " has ended.");
io.sockets.in(this.name).emit('gameEnd');
//End the game!
this.gameOn = false;
//Add the game to the WebUI team's SQL DB for data processing.
addGameDataToSQLDB(this.players[0].name, this.players[1].name, this.score[0],
this.score[1], winner);
//Put cached information into mongo database for replays.
this.emitReplay();
}
}
/*
* This is the function that calculates the differential in the game state,
* and it is called once every INTERVAL as determined by the startGame method.
*
*/
Room.prototype.ballLogic = function(){
/* Ball bouncing logic */
if(this.ballPos[1] - this.ballR < 0 ||
this.ballPos[1] + this.ballR > this.fieldSize[1]){
//change gBallPos[1] direction if you go off screen in y direction ....
this.ballV[1] = -this.ballV[1];
}
/* Paddle Boundary Logic */
/* Left paddle */
if(this.ballPos[0] == this.paddleSize[0] &&
//Left paddle's x
this.ballPos[1] >= this.players[0].paddlePos &&
this.ballPos[1] <= (this.players[0].paddlePos + this.paddleSize[1])) //Left paddle's y range
{
this.ballV[0] = -this.ballV[0]; //changes x direction
}
else if(this.ballPos[0] < this.paddleSize[0] &&
this.ballPos[0] > 0 && //X boundary of the edges
(this.ballPos[1] == this.players[0].paddlePos ||
//Y boundary
this.ballPos[1] == (this.players[0].paddlePos + this.paddleSize[1]))){ //top and bottom of left paddle
this.ballV[1] = -this.ballV[1]; //changes y direction
}
/* Right paddle */
if(this.ballPos[0] == this.fieldSize[0] - this.paddleSize[0] && //Right paddle's x
this.ballPos[1] >= this.players[1].paddlePos &&
this.ballPos[1] <= (this.players[1].paddlePos + this.paddleSize[1])) //Right paddle's y range
{
this.ballV[0] = -this.ballV[0]; // changes x direction
}
else if(this.ballPos[0] > this.fieldSize[0] - this.paddleSize[0] &&
this.ballPos[0] < this.fieldSize[0] && //X boundary of the edges
(this.ballPos[1] == this.players[1].paddlePos ||
this.ballPos[1] == (this.players[1].paddlePos + this.paddleSize[1])) //Y boundary of the edges
){ //top and bottom of right paddle
this.ballV[1] = -this.ballV[1]; // changes y direction
}
// if ball goes out of frame reset in the middle and put to default speed and increment gScore...
if(this.ballPos[0] + this.ballR < 0){
//changed these numbers -- ball was going super far out of frame
this.ballPos[0] = this.fieldSize[0]/2;
//Randomize starting y position to account for boundary issues
this.ballPos[1] = Math.floor(Math.random()*(this.fieldSize[1]-2))+1;
this.ballV[0] = -1; // Changes the direction of the ball if Player 2 scored
var dir = Math.floor(Math.random()*2);// random direction variable(1 or 0)
if(dir == 0){
dir = -2;//ball direction is up
}else{
dir = 2;//ball direction is down
}
this.ballV[1] = dir;
this.score[1] = this.score[1] + 1;
this.sendScore();
}
if(this.ballPos[0] + this.ballR > this.fieldSize[0] + 10){
this.ballPos[0] = this.fieldSize[0]/2;
this.ballPos[1] = Math.floor(Math.random()*(this.fieldSize[1]-2))+1;
this.ballV[0] = 1;
//random direction variable(1 or 0)
var dir = Math.floor(Math.random()*2);
if(dir == 0){
dir = -2;//ball direction is up
}else{
dir = 2;//ball direction is down
}
this.ballV[1] = dir;
this.score[0] = this.score[0] + 1;
//Sends score.
this.sendScore();
}
//Updates ball position based on velocity.
this.ballPos[0]+=this.ballV[0];
this.ballPos[1]+=this.ballV[1];
}
/*
* Generates game ID based on UTC
*/
Room.prototype.genGameID = function(){
//Temporarily using unix time for gameID - highly unlikely to have duplicates
var foo = new Date;
var unixtime = parseInt(foo.getTime());
this.rGameID = unixtime;
}
/**
* Emits current cached game information to mongodb replay database.
*/
Room.prototype.emitReplay = function(){
//Saves the game information as a Javascript object, will retain its shape
//However, these will simply become objects, must be casted on querying
rDB.replays.save({gameID: this.rGameID, replayDocs: this.rDocs});
}