diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml
index f698223..674cd5f 100644
--- a/.idea/codeStyleSettings.xml
+++ b/.idea/codeStyleSettings.xml
@@ -15,6 +15,9 @@
+
+
+
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 0b14868..8a91c4c 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -2,6 +2,7 @@
+
\ No newline at end of file
diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml
index 0b732de..a9b7b7a 100644
--- a/.idea/jsLibraryMappings.xml
+++ b/.idea/jsLibraryMappings.xml
@@ -1,7 +1,6 @@
-
diff --git a/.idea/pong-mmo-server.iml b/.idea/pong-mmo-server.iml
new file mode 100644
index 0000000..0c8e8cc
--- /dev/null
+++ b/.idea/pong-mmo-server.iml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/game/socket/pongSocket.js b/game/socket/pongSocket.js
index bb55ec8..ed2f49c 100644
--- a/game/socket/pongSocket.js
+++ b/game/socket/pongSocket.js
@@ -12,6 +12,7 @@
* --------
* Copyright 2012 Konstantin Raev (bestander@gmail.com)
*/
+// TODO test garbage collection of sockets and games
'use strict';
function PongSocket (socket, lobby){
@@ -26,13 +27,14 @@ function PongSocket (socket, lobby){
this._lobby = lobby;
this._game = null;
this._playerId = null;
+ this._matchStarted = false;
this._defineCommandsHandlers();
}
module.exports = PongSocket;
-// world update rate in milliseconds
-PongSocket.prototype.GAME_UPDATE_PERIOD_MILLIS = 1000;
+// match update rate in milliseconds
+PongSocket.prototype.MATCH_UPDATE_PERIOD_MILLIS = 1000;
PongSocket.prototype._defineCommandsHandlers = function () {
var that = this;
@@ -41,40 +43,72 @@ PongSocket.prototype._defineCommandsHandlers = function () {
// TODO will be async I'm pretty sure
that._game = that._lobby.getGame();
that._playerId = that._game.joinPlayer();
- that._socket.emit('ENTERED_GAME', that._game.getParametersAndState());
+ that._socket.emit('GAME_ENTERED', that._game.getFieldParams());
+ that._defineGameEventsHandlers();
}
});
this._socket.on('LAG_CHECK', function () {
- that._socket.emit('LAG_CHECK_RESPONSE', new Date().getTime());
+ that._socket.emit('LAG_RESPONSE', new Date().getTime());
});
this._socket.on('READY', function () {
if (that._isJoinedToGame()) {
that._game.handlePlayerCommand(that._playerId, 'READY');
- that._startClientNotificationLoop();
+ }
+ });
+ this._socket.on('PLAYER_COMMAND', function (data) {
+ if (that._isJoinedToGame()) {
+ that._game.handlePlayerCommand(that._playerId, data);
}
});
this._socket.on('disconnect', function () {
if (that._isJoinedToGame()) {
- that._game.quitPlayer(that._playerId);
+ that._game.quitPlayer(that._playerId);
}
});
+};
+PongSocket.prototype._defineGameEventsHandlers = function () {
+ var that = this;
+ this._game.getEventEmitter().on('PLAYER_JOINED', function (data) {
+ that._socket.emit('PLAYER_JOINED', data);
+ });
+ this._game.getEventEmitter().on('PLAYER_QUIT', function (data) {
+ that._socket.emit('PLAYER_QUIT', data);
+ });
+ this._game.getEventEmitter().on('PLAYER_READY', function (data) {
+ that._socket.emit('PLAYER_READY', data);
+ });
+ this._game.getEventEmitter().on('PLAYER_SCORED', function (data) {
+ that._socket.emit('PLAYER_SCORED', data);
+ });
+ this._game.getEventEmitter().on('MATCH_STARTED', function (data) {
+ that._matchStarted = true;
+ that._startClientNotificationLoop();
+ that._socket.emit('MATCH_STARTED', data);
+ });
+ this._game.getEventEmitter().on('MATCH_STOPPED', function (data) {
+ that._matchStarted = false;
+ that._socket.emit('MATCH_STOPPED', data);
+ });
};
PongSocket.prototype._isJoinedToGame = function () {
return this._game && this._playerId;
};
+PongSocket.prototype._isMatchStarted = function () {
+ return this._isJoinedToGame() && this._matchStarted;
+};
+
PongSocket.prototype._startClientNotificationLoop = function () {
this._boundLoopCall = this._boundLoopCall || this._startClientNotificationLoop.bind(this);
- if(this._isJoinedToGame()){
+ if(this._isMatchStarted()){
this._socket.emit('GAME_UPDATE', {
'objects': this._game.getObjectPositions(),
'time': new Date().getTime()
});
- setTimeout(this._boundLoopCall, this.GAME_UPDATE_PERIOD_MILLIS);
-
+ setTimeout(this._boundLoopCall, this.MATCH_UPDATE_PERIOD_MILLIS);
}
};
diff --git a/test/pongSocket.spec.js b/test/pongSocket.spec.js
index 366e069..7d848a6 100644
--- a/test/pongSocket.spec.js
+++ b/test/pongSocket.spec.js
@@ -4,6 +4,9 @@
*
* To execute run 'jasmine-node test --verbose --forceexit' from project root
*/
+/*jshint node:true indent:2*/
+/*global it:true describe:true expect:true spyOn:true beforeEach:true afterEach:true jasmine:true runs waitsFor*/
+
'use strict';
var PongSocket = require('../game/socket/pongSocket.js');
@@ -11,7 +14,7 @@ var EventEmitter = require('events').EventEmitter;
var _ = require('lodash');
// timeout hack for Node.js
-jasmine.getGlobal().setTimeout = function(funcToCall, millis) {
+jasmine.getGlobal().setTimeout = function (funcToCall, millis) {
if (jasmine.Clock.installed.setTimeout.apply) {
return jasmine.Clock.installed.setTimeout.apply(this, arguments);
} else {
@@ -19,36 +22,41 @@ jasmine.getGlobal().setTimeout = function(funcToCall, millis) {
}
};
-describe('Pong Socket class', function () {
+describe('When Pong Socket', function () {
var gameMock = {};
var socket_io;
var gameLobbyMock = {};
+ var fieldParams = {'gameParams': 'mock'};
+ var gameEvents;
beforeEach(function () {
+
jasmine.Clock.useMock();
socket_io = new EventEmitter();
socket_io.disconnected = false;
-
+
spyOn(socket_io, 'emit').andCallThrough();
-
- gameMock = jasmine.createSpyObj('gameMock', ['quitPlayer', 'getEventEmitter', 'handlePlayerCommand']);
- gameMock.joinPlayer = function () {
+
+ gameMock = jasmine.createSpyObj('gameMock', ['quitPlayer', 'joinPlayer', 'getEventEmitter', 'handlePlayerCommand',
+ 'getObjectPositions', 'getFieldParams']);
+
+ gameMock.joinPlayer.andCallFake(function () {
// increment player id
this.playerId = this.playerId || 0;
this.playerId += 1;
return this.playerId;
- };
- gameMock.getObjectPositions = function () {
+ });
+ gameMock.getObjectPositions.andCallFake(function () {
return {};
- };
- gameMock.getParametersAndState = function () {
- return {'gameParams' : 'mock'};
- };
- spyOn(gameMock, 'joinPlayer').andCallThrough();
- spyOn(gameMock, 'getObjectPositions').andCallThrough();
- spyOn(gameMock, 'getParametersAndState').andCallThrough();
+ });
+ gameMock.getFieldParams.andCallFake(function () {
+ return fieldParams;
+ });
+ gameEvents = new EventEmitter();
+ gameMock.getEventEmitter.andReturn(gameEvents);
+
gameLobbyMock.getGame = function () {
return gameMock;
@@ -60,23 +68,24 @@ describe('Pong Socket class', function () {
socket_io.emit('disconnect');
});
- it('should throw error if it is created with a socket not in "connected" state', function () {
- socket_io.disconnected = undefined;
- var throwing = function () {
- new PongSocket(socket_io);
- };
-
- expect(throwing).toThrow(new Error('Socket is not connected'));
- });
+ describe('is created', function () {
+ it('it should throw error if socket is not in "connected" state or lobby is missing', function () {
+ socket_io.disconnected = undefined;
+ var throwing = function () {
+ var socket = new PongSocket(socket_io, gameLobbyMock);
+ };
+ expect(throwing).toThrow(new Error('Socket is not connected'));
+ socket_io.disconnected = false;
+ throwing = function () {
+ var socket = new PongSocket(socket_io, undefined);
+ };
+ expect(throwing).toThrow(new Error('No game lobby provided'));
+ });
- it('should expect a game lobby in constructor', function () {
- var throwing = function () {
- new PongSocket(socket_io, undefined);
- };
- expect(throwing).toThrow(new Error('No game lobby provided'));
});
- describe('when handling client messages', function () {
+
+ describe('receives from client message', function () {
var testLobby;
var lobbyReturnedGame;
@@ -92,10 +101,10 @@ describe('Pong Socket class', function () {
});
});
- describe('on "START_GAME"', function () {
-
- it('should request a new game from lobby', function () {
- new PongSocket(socket_io, testLobby);
+ describe('"START_GAME"', function () {
+
+ it('it should request a new game from lobby', function () {
+ var socket = new PongSocket(socket_io, testLobby);
runs(function () {
socket_io.emit('START_GAME');
@@ -104,11 +113,11 @@ describe('Pong Socket class', function () {
waitsFor(function () {
return lobbyReturnedGame;
}, 'getGame should have been called', 100);
-
+
});
- it('should not request a new game from a lobby twice', function () {
- new PongSocket(socket_io, testLobby);
+ it('it should ignore second START_GAME a new game was requested already', function () {
+ var socket = new PongSocket(socket_io, testLobby);
runs(function () {
socket_io.emit('START_GAME');
@@ -124,8 +133,8 @@ describe('Pong Socket class', function () {
});
});
- it('should call game.joinPlayer for the game from lobby', function () {
- new PongSocket(socket_io, testLobby);
+ it('it should call game.joinPlayer for the returned game from lobby', function () {
+ var socket = new PongSocket(socket_io, testLobby);
runs(function () {
socket_io.emit('START_GAME');
@@ -134,33 +143,33 @@ describe('Pong Socket class', function () {
waitsFor(function () {
return lobbyReturnedGame;
}, 'getGame should have been called', 100);
-
+
runs(function () {
expect(gameMock.joinPlayer).toHaveBeenCalled();
});
-
+
});
- it('should respond with "ENTERED_GAME" message', function () {
- new PongSocket(socket_io, testLobby);
+ it('it should respond with "GAME_ENTERED" message with field dimensions', function () {
+ var socket = new PongSocket(socket_io, testLobby);
socket_io.emit('START_GAME');
jasmine.Clock.tick(1);
var response = _.filter(socket_io.emit.calls, function (elem) {
- return elem.args[0] === 'ENTERED_GAME'
+ return elem.args[0] === 'GAME_ENTERED';
});
expect(response.length).toBe(1);
expect(response[0].args.length).toBe(2);
- expect(response[0].args[1]).toEqual({'gameParams': 'mock'});
+ expect(response[0].args[1]).toEqual(fieldParams);
});
});
- describe('on "LAG_CHECK"', function () {
- it('should return current server time', function () {
+ describe('"LAG_CHECK"', function () {
+ it('it should return current server time', function () {
var response;
- socket_io.on('LAG_CHECK_RESPONSE', function (data) {
+ socket_io.on('LAG_RESPONSE', function (data) {
response = data;
});
- new PongSocket(socket_io, gameLobbyMock);
+ var socket = new PongSocket(socket_io, gameLobbyMock);
runs(function () {
socket_io.emit('LAG_CHECK');
@@ -171,21 +180,21 @@ describe('Pong Socket class', function () {
}, 'Should have responded on LAG_CHECK', 100);
runs(function () {
- expect(response / 100).toBeCloseTo(new Date().getTime() / 100, 0);
+ expect(response).toBeCloseTo(new Date().getTime(), -1);
});
- });
+ });
});
- describe('on "READY"', function () {
- it('should be ignored if it was called before START_GAME', function () {
- new PongSocket(socket_io, testLobby);
+ describe('"READY"', function () {
+ it('it should be ignored if game was not joined', function () {
+ var socket = new PongSocket(socket_io, testLobby);
socket_io.emit('READY');
expect(gameMock.handlePlayerCommand).not.toHaveBeenCalled();
});
- it('should pass the ready command to game object', function () {
- new PongSocket(socket_io, testLobby);
+ it('it should pass the "ready" command to game object', function () {
+ var socket = new PongSocket(socket_io, testLobby);
socket_io.emit('START_GAME');
expect(gameMock.handlePlayerCommand).not.toHaveBeenCalled();
socket_io.emit('READY');
@@ -195,50 +204,196 @@ describe('Pong Socket class', function () {
});
});
- describe('on disconnect', function () {
+ describe('"PLAYER_COMMAND"', function () {
+ it('it should be ignored if game was not joined', function () {
+ var socket = new PongSocket(socket_io, testLobby);
+ socket_io.emit('PLAYER_COMMAND');
+ expect(gameMock.handlePlayerCommand).not.toHaveBeenCalled();
+ });
- it('should call game.quitPlayer', function () {
- new PongSocket(socket_io, testLobby);
+ it('it should pass the command to game object', function () {
+ var socket = new PongSocket(socket_io, testLobby);
socket_io.emit('START_GAME');
- expect(gameMock.quitPlayer).not.toHaveBeenCalledWith(gameMock.playerId);
+ expect(gameMock.handlePlayerCommand).not.toHaveBeenCalled();
+ var command = {command: 115};
+ socket_io.emit('PLAYER_COMMAND', command);
+ var playerId = gameMock.playerId;
+ expect(playerId).toEqual(1);
+ expect(gameMock.handlePlayerCommand).toHaveBeenCalledWith(playerId, command);
+ });
+ });
+
+ describe('disconnect', function () {
+
+ it('it should call game.quitPlayer', function () {
+ var socket = new PongSocket(socket_io, testLobby);
+ socket_io.emit('START_GAME');
+ expect(gameMock.quitPlayer).not.toHaveBeenCalled();
socket_io.emit('disconnect');
expect(gameMock.quitPlayer).toHaveBeenCalledWith(gameMock.playerId);
});
+ it('it should do nothing if no game was joined', function () {
+ var socket = new PongSocket(socket_io, testLobby);
+ socket_io.emit('disconnect');
+ expect(gameMock.quitPlayer).not.toHaveBeenCalled();
+ });
});
-
});
- describe('when joined a game', function () {
+ describe('receives from Game event', function () {
+ describe('PLAYER_JOINED, PLAYER_QUIT, PLAYER_READY, PLAYER_SCORED', function () {
+ it('it sends those events to client', function () {
+ var messageArgs;
+ var message;
+ var socket;
+ socket = new PongSocket(socket_io, gameLobbyMock);
+ socket_io.emit('START_GAME');
+
+ message = function () {
+ return _.filter(socket_io.emit.calls, function (elem) {
+ return elem.args[0] === 'PLAYER_JOINED';
+ });
+ };
+ messageArgs = {type: 'left', name: 'Bob'};
+ expect(message().length).toBe(0);
+ gameEvents.emit('PLAYER_JOINED', messageArgs);
+ expect(message().length).toBe(1);
+ expect(_.last(message()).args[1]).toBe(messageArgs);
+
+ message = function () {
+ return _.filter(socket_io.emit.calls, function (elem) {
+ return elem.args[0] === 'PLAYER_QUIT';
+ });
+ };
+ messageArgs = {type: 'left'};
+ expect(message().length).toBe(0);
+ gameEvents.emit('PLAYER_QUIT', messageArgs);
+ expect(message().length).toBe(1);
+ expect(_.last(message()).args[1]).toBe(messageArgs);
+
+ message = function () {
+ return _.filter(socket_io.emit.calls, function (elem) {
+ return elem.args[0] === 'PLAYER_READY';
+ });
+ };
+ messageArgs = {type: 'left'};
+ expect(message().length).toBe(0);
+ gameEvents.emit('PLAYER_READY', messageArgs);
+ expect(message().length).toBe(1);
+ expect(_.last(message()).args[1]).toBe(messageArgs);
+
+ message = function () {
+ return _.filter(socket_io.emit.calls, function (elem) {
+ return elem.args[0] === 'PLAYER_SCORED';
+ });
+ };
+ messageArgs = {type: 'left'};
+ expect(message().length).toBe(0);
+ gameEvents.emit('PLAYER_SCORED', messageArgs);
+ expect(message().length).toBe(1);
+ expect(_.last(message()).args[1]).toBe(messageArgs);
+ });
+ });
+ describe('MATCH_STARTED', function () {
+ it('it sends the event to client', function () {
+ var message;
+ var socket;
+ socket = new PongSocket(socket_io, gameLobbyMock);
+ socket_io.emit('START_GAME');
+
+ message = function () {
+ return _.filter(socket_io.emit.calls, function (elem) {
+ return elem.args[0] === 'MATCH_STARTED';
+ });
+ };
- function getUpdateMessages () {
- return _.filter(socket_io.emit.calls, function (elem) {
- return elem.args[0] === 'GAME_UPDATE'
+ expect(message().length).toBe(0);
+ gameEvents.emit('MATCH_STARTED');
+ expect(message().length).toBe(1);
+ expect(_.last(message()).args[1]).toBeUndefined();
});
- }
- it('should notify connected client about world object positions at regular time periods', function () {
- var socket = new PongSocket(socket_io, gameLobbyMock);
-
- socket_io.emit('START_GAME');
- jasmine.Clock.tick(socket.GAME_UPDATE_PERIOD_MILLIS * 3);
+ it('starts notifying client about game object positions at regular intervals', function () {
+ function getUpdateMessages() {
+ return _.filter(socket_io.emit.calls, function (elem) {
+ return elem.args[0] === 'GAME_UPDATE';
+ });
+ }
+
+ var socket = new PongSocket(socket_io, gameLobbyMock);
+
+ socket_io.emit('START_GAME');
+ jasmine.Clock.tick(socket.MATCH_UPDATE_PERIOD_MILLIS * 3);
- expect(getUpdateMessages().length).toEqual(0);
- expect(gameMock.getObjectPositions).not.toHaveBeenCalled();
- socket_io.emit('READY');
+ expect(getUpdateMessages().length).toEqual(0);
+ expect(gameMock.getObjectPositions).not.toHaveBeenCalled();
- expect(getUpdateMessages().length).toEqual(1);
- jasmine.Clock.tick(socket.GAME_UPDATE_PERIOD_MILLIS - 10);
+ gameEvents.emit('MATCH_STARTED');
+ expect(getUpdateMessages().length).toEqual(1);
+ jasmine.Clock.tick(socket.MATCH_UPDATE_PERIOD_MILLIS - 10);
- jasmine.Clock.tick(socket.GAME_UPDATE_PERIOD_MILLIS);
- var updates = getUpdateMessages();
- expect(updates.length).toEqual(2);
- expect(_.last(updates).args[1].time).toBeDefined();
- expect(_.last(updates).args[1].objects).toBeDefined();
- expect(gameMock.getObjectPositions).toHaveBeenCalled();
+ jasmine.Clock.tick(socket.MATCH_UPDATE_PERIOD_MILLIS);
+ var updates = getUpdateMessages();
+ expect(updates.length).toEqual(2);
+ expect(_.last(updates).args[1].time).toBeDefined();
+ expect(_.last(updates).args[1].objects).toBeDefined();
+ expect(gameMock.getObjectPositions).toHaveBeenCalled();
+ });
});
- });
+ describe('MATCH_STOPPED', function () {
+ it('it sends the event to client', function () {
+ var message;
+ var socket;
+ socket = new PongSocket(socket_io, gameLobbyMock);
+ socket_io.emit('START_GAME');
+
+ message = function () {
+ return _.filter(socket_io.emit.calls, function (elem) {
+ return elem.args[0] === 'MATCH_STOPPED';
+ });
+ };
+
+ expect(message().length).toBe(0);
+ gameEvents.emit('MATCH_STOPPED');
+ expect(message().length).toBe(1);
+ expect(_.last(message()).args[1]).toBeUndefined();
+ });
+
+ it('stops notifying client about game object positions at regular intervals', function () {
+ function getUpdateMessages() {
+ return _.filter(socket_io.emit.calls, function (elem) {
+ return elem.args[0] === 'GAME_UPDATE';
+ });
+ }
+
+ var socket = new PongSocket(socket_io, gameLobbyMock);
+
+ socket_io.emit('START_GAME');
+ jasmine.Clock.tick(socket.MATCH_UPDATE_PERIOD_MILLIS * 3);
+
+ expect(getUpdateMessages().length).toEqual(0);
+ expect(gameMock.getObjectPositions).not.toHaveBeenCalled();
+
+ gameEvents.emit('MATCH_STARTED');
+ expect(getUpdateMessages().length).toEqual(1);
+ jasmine.Clock.tick(socket.MATCH_UPDATE_PERIOD_MILLIS - 10);
+
+ jasmine.Clock.tick(socket.MATCH_UPDATE_PERIOD_MILLIS);
+ var updates = getUpdateMessages();
+ expect(updates.length).toEqual(2);
+ expect(_.last(updates).args[1].time).toBeDefined();
+ expect(_.last(updates).args[1].objects).toBeDefined();
+ expect(gameMock.getObjectPositions).toHaveBeenCalled();
+ gameEvents.emit('MATCH_STOPPED');
+ expect(getUpdateMessages().length).toEqual(2);
+ jasmine.Clock.tick(socket.MATCH_UPDATE_PERIOD_MILLIS * 5);
+ expect(getUpdateMessages().length).toEqual(2);
+
+ });
+ });
+ });
});