Source: shared/session.js

/* 
* To use this with require.js AND the node.js module system (on server and client side).
* see https://github.com/jrburke/amdefine
*/
if (typeof define !== 'function') { var define = require('amdefine')(module); }


/**
 * @module shared/session
 * @private
 */
define(function(require, exports, module) {
	'use strict';

	var EventEmitter = require('events').EventEmitter;
	var util = require('util');
	var playerModule = require('./player');


	/**
	 * @typedef {Object} SessionOptions
	 * @property {string} [scriptName] name of server side script file that should
	 *  be executed when a new session is created. This module must export a 
	 *  constructor that takes a session as its only argument (module.exports = 
	 *  function (session)). This property has to contain only numbers (0-9) and 
	 *  letters (A-Za-z) for security reasons. Multi will look for the given module
	 *  name inside a 'games' directory that lies next to the multi directory.
	 * @property {string} [token.func='numeric']  name of a function inside the 
	 *  {@link module:server/token} module that should generate the session token
	 * @property {Array}  [token.args=[]]   argument array for the token 
	 *  generationfunction
	 * @property {integer}[minPlayerNeeded=1] minimum number of players needed 
	 *  for this session
	 * @property {integer}[maxPlayerAllowed=10] maximum number of players allowed 
	 *  for this session. Every addition player won't be allowed to join the session.
	 * @property {Array.<string>}[filter] list of names of filter functions. 
	 *  The functions have to be defined in {@link module:server/filter} and will 
	 *  then be used to filter outgoing server-messages.
	 */


	/**
	 * @classdesc A game session that connects and manages multiple players.
	 *
	 * @mixes external:EventEmitter
	 * @memberof module:shared/session
	 * @class
	 * @inner
	 * @protected
	 *
	 * @see module:client/session~Session
	 * @see module:server/session~Session
	 *
	 * @fires module:shared/session~Session#playerJoined
	 * @fires module:shared/session~Session#playerLeft
	 * @fires module:shared/session~Session#destroyed
	 * @fires module:shared/session~Session#belowMinPlayerNeeded
	 * @fires module:shared/session~Session#aboveMinPlayerNeeded
	 */
	var Session = function (messageBus) {

		EventEmitter.call(this);

		// PUBLIC
		/** 
		 * unique token identifying this session
		 * @type {string}
		 * @readonly
		 */
		this.token = null;
		/**
		 * Dictionary of all players currently connected
		 * to this session mapped on their ids.
		 * @type {Object.<string, module:shared/player~Player>}
		 * @readonly
		 */
		this.players = {};
		/**
		 * @see SessionOptions
		 * @readonly
		 */
		this.minPlayerNeeded = 1;
		/**
		 * @see SessionOptions
		 * @readonly
		 */
		this.maxPlayerAllowed = 10;
		/**
		 * if false no more clients are allowed to join this session
		 * @private
		 */
		this.playerJoiningEnabled = true;


		// PROTECTED
		this.messageBus = null;
		this.messageSender = null;
	};

	util.inherits(Session, EventEmitter);

	/**
	 * Child classes should call this method when they are finished building 
	 * and are ready to add listeners to themselves.
	 * @protected
	 */
	Session.prototype.onSessionReady = function () {
		var session = this;

		this.messageBus.register('message', 'session', function (message) {
			session.emit(message.type,  { type: message.type, data: message.data });
		});

		this.messageBus.register('changePlayerJoining', 'session', function (message) {
			session.playerJoiningEnabled = message.playerJoiningEnabled;
		});
	};

	/**
	 * Deconstructs this session when no longer needed and informs listening
	 * objects.
	 * @protected
	 */
	Session.prototype.destroy = function () {
		this.emit('destroyed');
		this.messageBus.unregisterAll();
		this.removeAllListeners();
	};

	/**
	 * Adds the given player to this session. Override if needed.
	 * @param player {module:shared/player~Player} player instance to add
	 * @fires module:shared/session~Session#playerJoined
	 * @protected
	 */
	Session.prototype.addPlayer = function (player) {
		var session = this;

		// add to collection
		this.players[player.id] = player;

		// add listeners
		player.on('disconnected', function () {
			session.removePlayer(player);
		});

		// inform others about this player
		session.emit('playerJoined', { player: player });
		if (session.getPlayerCount() === session.minPlayerNeeded) {
			session.emit('aboveMinPlayerNeeded');
		}
		if (session.getPlayerCount() === session.maxPlayerAllowed) {
			session.emit('aboveMaxPlayerNeeded');
		}
	};

	/**
	 * Removes the given player from this session. Override if needed.
	 * @param player {module:shared/player~Player} player instance to remove
	 * @fires module:shared/session~Session#playerRemoved
	 * @protected
	 */
	Session.prototype.removePlayer = function (player) {
		delete this.players[player.id];
		this.emit('playerLeft', { player: player });

		if (this.getPlayerCount() === (this.minPlayerNeeded-1)) {
			this.emit('belowMinPlayerNeeded');
		}
		if (this.getPlayerCount() === (this.maxPlayerNeeded-1)) {
			this.emit('belowMaxPlayerNeeded');
		}
	};

	/**
	 * @returns {Array.<module:shared/player~Player>} an array of all 
	 * players currently connected to this session.
	 * The array is sorted by 
	 * {@link module:shared/player~Player#number player numbers} 
	 * from small to high.
	 */
	Session.prototype.getPlayerArray = function () {
		var playerArray = [];
		for(var i in this.players) {
			playerArray.push(this.players[i]);
		}
		return playerArray.sort(playerModule.compare);
	};

	/**
	 * @return {integer} number of currently connected players including myself
	 */
	Session.prototype.getPlayerCount = function () {
		return Object.keys(this.players).length;
	};

	/**
	 * @returns {module:shared/player~Player} the player with the
	 * given {@link module:shared/player~Player#number player numbers} 
	 * (even if this is myself) or null if no player with this number 
	 * exists
	 */
	Session.prototype.getPlayerByNumber = function (number) {
		var players = this.getPlayerArray().filter(function (player) {
			return player.number === number;
		});
		return players[0] || null;
	};

	/**
	 * @returns {module:shared/player~Player} the player with the
	 * given {@link module:shared/player~Player#id id} 
	 * (even if this is myself) or null if no player with this id 
	 * exists
	 */
	Session.prototype.getPlayerById = function (id) {
		return this.players[id] || null;
	};

	/**
	 * @return {boolean} true if there are as many ore more players 
	 * connected to this session as are allowed
	 */
	Session.prototype.isFull = function () {
		return this.getPlayerCount() >= this.maxPlayerAllowed;
	};

	/**
	 * When you call this new players are not allowed to join this
	 * session any more. Instead their promise will be rejected with a 
	 * {@link module:shared/errors.JoiningDisabledError JoiningDisabledError}.
	 */
	Session.prototype.disablePlayerJoining = function () {
		this.playerJoiningEnabled = false;
		this.messageBus.send({
			name: 'changePlayerJoining',
			fromInstance: 'session',
			playerJoiningEnabled: false
		});
	};

	/**
	 * A call to this method will allow new players to join this session
	 * again.
	 */
	Session.prototype.enablePlayerJoining = function () {
		this.playerJoiningEnabled = true;
		this.messageBus.send({
			name: 'changePlayerJoining',
			fromInstance: 'session',
			playerJoiningEnabled: true
		});
	};

	/**
	 * Sends the given message to all other instances of this session.
	 * @param {string} type    type of message that should be send
	 * @param {object} [data]  message data that should be send
	 * @param {module:client/multi~toClient|module:server/multi~toClient} 
	 *  [toClient='all']  which client should receive this message
	 * @param {boolean} [volatile=false]  if true, the message may be dropped
	 *  by the framework. Use this option for real time data where one dropped
	 *  message does not interrupt your application.
	 * @example
	 * // on client no 1 or server
	 * session.on('ping', function (event) {
	 *   // outputs 'bar'
	 *   console.log(event.data.foo);
	 * });
	 * // on client no 2 or server, instance of same session
	 * session.message('ping', { foo: 'bar' });
	 */
	Session.prototype.message = function (type, data, toClient, volatile) {
		this.messageSender.message(type, data, toClient, volatile);
	};

	/**
	 * Prepares this session and all its players for sending 
	 * it via socket message while avoiding circular dependencies.
	 * @return {object} serialized session object including players
	 */
	Session.prototype.serialize = function() {
		var players = [];
		for (var i in this.players) {
			players.push(this.players[i].serialize());
		}
		return {
			token: this.token,
			players: players,
			minPlayerNeeded: this.minPlayerNeeded,
			maxPlayerAllowed: this.maxPlayerAllowed
		};
	};

	/**
	 * Fired when this session is no longer valid. <br>
	 * The reason could be a broken connection or the
	 * removal of your own player. <br><br>
	 * Don't use this session any longer after the event 
	 * has been fired.
	 * @event module:shared/session~Session#destroyed
	 */

	/**
	 * Fired when a new player has been added to this session.
	 * From now on you can safely communicate with this player.
	 * @event module:shared/session~Session#playerJoined
	 * @property {module:shared/player~Player} player  The newly added player.
	 * @example <caption>Adding connected players to the DOM</caption>
	 * session.on('playerJoined', function (event) {
	 *   var playerDiv = $('#player').clone();
	 *   $('#players').append(playerDiv);
	 *   event.player.on('disconnected', function () {
	 *     playerDiv.remove();
	 *   });
	 * }
	 */

	/**
	 * Fired when a player has been removed from this session.
	 * @event module:shared/session~Session#playerLeft
	 * @property {module:shared/player~Player} player  The removed player.
	 */

	/**
	 * Fired when a player has been removed from this session and
	 * there are now less player connected to this session than stated 
	 * in minPlayerNeeded.<br><br>
	 * You could listen for this event to stop a running game when
	 * the player count is getting to low.
	 * @event module:shared/session~Session#belowMinPlayerNeeded
	 */

	/**
	 * Fired when a new player has been added to this session and
	 * there are now exactly as many players connected to this session
	 * as stated in minPlayerNeeded.<br><br>
	 * You could listen for this event to start your game when
	 * enough players have connected.
	 * @event module:shared/session~Session#aboveMinPlayerNeeded
	 */

	/**
	 * Fired when a new player has been added to this session and
	 * there are now exactly as many players connected to this session
	 * as stated in maxPlayerAllowed.
	 * @event module:shared/session~Session#aboveMaxPlayerAllowed
	 */

	/**
	 * Fired when a player has been removed from this session and
	 * there are now less player connected to this session than stated 
	 * in maxPlayerNeeded.
	 * @event module:shared/session~Session#belowMaxPlayerAllowed
	 */


	exports.Session = Session;
	return exports;

});