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); + + }); + }); + }); });