serverjs/Queue/QueueManager.js

'use strict';

const 
	Log = require('../logger'),
	Queue = reqfromroot('Queue/Queue');

class QueueManager{
	
	/**
	* Менеджер очередей и игр.
	* @param  {Server} server      сервер
	* @param  {object} quickConfig настройки быстрой игры
	*/
	constructor(server, quickConfig, quickRules){

		/**
		* Сервер.
		* @type {Server}
		*/
		this.server = server;

		/**
		* Логгер.
		* @type {winston.Logger}
		*/
		this.log = Log(module, null, quickConfig.debug);

		/**
		* Настройки быстрой игры.
		* @type {object}
		*/
		this.quickQueueConfig = quickConfig;


		this.quickQueueRules = quickRules;

		/**
		* Запущенные игры.
		* @type {Object}
		*/
		this.games = {};

		/**
		* Все очереди.
		* @type {Object}
		*/
		this.queues = {};

		/**
		* Очереди, в которые могут подключаться игроки (с `type` не равным `private` или `botmatch`).  
		* На время прохождения игр очереди убираются из этого списка.
		* @type {Array}
		*/
		this.queueList = [];

		this.queueListAll = [];

		/**
		* Очереди быстрой игры, в которые могут подключаться игроки (с `type` равным `quick`).  
		* На время прохождения игр быстрые очереди убираются из этого списка.
		* @type {Array}
		*/
		this.quickQueues = [];

		/**
		* Случайные имена ботов.
		* @type {Array}
		*/
		this.randomNames = [
			'John Snow','Gandalf','Joel','Ellie','Batman','Joker','Yoh','Geralt','Cirilla','Triss','Yennefer','Harry',
			'Hermione','Ron','Arya','Sherlock','Neo','Marty McFly','Chester','Jodie','Aiden','Aloy','The Doctor','Severus',
			'Alan','Regis','Nemo','Dio','Jesse','Heisenberg','Merlin','House M.D.','Bilbo','Legolas','Strider','Aragorn','Frodo',
			'Sam','Merry','Pippin','Tess','Kenny','Ted','Barney','Marshall','Robin','Lily','Oleg','Sergey','Georgy','Talion',
			'Johnny C.','Carl','Jotaro','Ken','Vanilla Ice','Zepelli','A Pig','Angela','Nataniel','Magnus','Leman','Malcador',
			'Saltzpyre', 'S. Sam', 'G-Man', 'Kerillian', 'Xan'
		];
	}

	/**
	* Возвращает список с информацией о существующих очередях.
	* @param {number}  page            номер страницы очередей
	* @param {number}  [pagination=10] во очередей на странице
	* @param {boolean} [hideStarted]   не добавлять очереди с начатыми играми
	*
	* @return {object[]} Возвращает массив с объектами с информацией об очередях.
	*/
	getQueueList(page, pagination, hideStarted){
		let list = [];

		if(typeof page != 'number' || isNaN(page) || page < 0){
			page = 0;
		}

		if(typeof pagination != 'number' || isNaN(pagination) || pagination <= 0 || pagination > 10){
			pagination = 10;
		}

		let pageLength = pagination;
		let queueList = hideStarted ? this.queueList : this.queueListAll;

		// Сколько элементов нужно будет пропустить
		let skip = page * pageLength;
		if(skip >= queueList.length){
			skip = Math.max(queueList.length - pageLength, 0);
			page = Math.max(queueList.length/pageLength, 1) - 1;
		}

		// Есть ли элементы перед и после текущей страницы
		let moreBefore = (skip > 0);
		let moreAfter = false;

		for(let i = 0, len = queueList.length; i < len; i++){

			// Пропускаем заданное кол-во элементов
			if(skip > 0){
				skip--;
				continue;
			}

			// Мы выбрали достаточное кол-во элементов
			if(pageLength <= 0){
				// Указываем, что есть элемент после последнего добавленного			
				moreAfter = true;
				break;
			}

			let queue = queueList[i];
			list.push(queue.info);

			pageLength--;
		}

		return {type: 'QUEUE_LIST', list, moreBefore, moreAfter, page, pagination};
	}

	/**
	* Создает очередь с заданными настройками и добавляет в нее игрока.
	* @param {Player} player      игрок
	* @param {string} gameMode    режим игры
	* @param {object} queueConfig настройки очереди
	* @param {object} [gameRules] настройки игры
	*/
	addCustomQueue(player, isPrivate, gameMode, queueConfig, gameRules){
		if(player.queue || player.game){
			return;
		}

		if(!queueConfig || typeof queueConfig != 'object'){
			player.recieveMenuNotification({type: 'QUEUE_INVALID'});
			return;
		}

		if(!this.server.gameModes.hasOwnProperty(gameMode)){
			player.recieveMenuNotification({type: 'QUEUE_INVALID'});
			return;
		}

		let gameClass = this.server.gameModes[gameMode];
		queueConfig.game = gameClass[0];
		queueConfig.bot = gameClass[1];
		queueConfig.debug = this.server.params.debug;
		queueConfig.name = player.name + '\'s Game';

		if(!gameRules || typeof gameRules != 'object'){
			gameRules = {};
		}

		let queue = this.addQueue(
			isPrivate ? 'private' : 'custom',
			queueConfig,
			gameRules
		);
		queue.addPlayer(player);
	}

	/**
	* Добавляет игрока в очередь по id очереди.
	* @param {Player} player игрок
	* @param {string} qid    id очереди
	*/
	addPlayerToCustomQueue(player, qid){
		if(this.queues.hasOwnProperty(qid)){
			this.queues[qid].addPlayer(player);
		}
		else{
			player.recieveMenuNotification({type: 'QUEUE_INACTIVE'});
		}
	}

	/**
	* Создает очередь со стандартными настройками и добавляет в нее игрока.
	* @param {Player} player игрок
	*/
	addPlayerToQuickQueue(player){
		let playerIsBusy = this.playerIsBusy(player);

		// Проверяем, не занят ли игрок уже
		if(playerIsBusy){
			if(playerIsBusy === QueueManager.PLAYER_STATUS.IN_GAME){
				this.reconnectPlayer(player);
			}
			return;
		}

		// Находим или создаем незаполненную быструю очередь
		let queue = this.quickQueues[0];
		if(!queue){
			queue = this.addQueue('quick', this.quickQueueConfig, this.quickQueueRules);
		}

		queue.addPlayer(player);
	}

	/**
	* Создает и добавляет новую очередь.
	* @param {string} type   Тип очереди.
	*                        Значения: `'quick', 'custom', 'private', 'botmatch'`
	* @param {object} config Конфигурация очереди.
	*
	* @return {Queue} Возвращает созданную очередь.
	*/
	addQueue(type, config, rules){
		config = Object.assign({}, config);
		rules = Object.assign({}, rules);

		config.decisionTime = this.server.params.decisionTime;

		let queue = new Queue(this, type, config, rules);

		this.queues[queue.id] = queue;
		this.addQueueToList(queue);

		return queue;
	}

	/**
	* Добавляет очередь в список очередей, к которым можно присоединиться.
	* @param {Queue} queue
	*/
	addQueueToList(queue){
		// Добавляем очередь в общий список
		if(queue.type != 'private' && queue.savedType != 'private'){
			let i = this.queueList.indexOf(queue);
			if(!~i){
				this.queueList.unshift(queue);
			}

			i = this.queueListAll.indexOf(queue);
			if(!~i){
				this.queueListAll.unshift(queue);
			}
		}

		// Добавляем очередь в список быстрых игр
		let i = this.quickQueues.indexOf(queue);
		if(!~i && queue.type == 'quick'){
			this.quickQueues.push(queue);
		}
	}

	/**
	* Удаляет очередь из менеджера.
	* @param  {Queue} queue
	*/
	removeQueue(queue){
		if(!this.queues[queue.id]){
			return;
		}
		this.removeQueueFromList(queue);
		delete this.queues[queue.id];
	}

	/**
	* Удаляет очередь из списка очередей к которым можно присоединиться.
	* @param  {Queue} queue
	*/
	removeQueueFromList(queue, inProgress){
		// Убираем очередь из общего списка
		let i = this.queueList.indexOf(queue);
		if(~i){
			this.queueList.splice(i, 1);
		}

		if(!inProgress || queue.type == 'private' || queue.savedType == 'private'){
			i = this.queueListAll.indexOf(queue);
			if(~i){
				this.queueListAll.splice(i, 1);
			}
		}

		// Убираем очередь из списка быстрых игр
		i = this.quickQueues.indexOf(queue);
		if(~i) {
			this.quickQueues.splice(i, 1);
		}
	}

	/**
	* Устанавливает статус соединения игрока на `false` и возвращает `false`, 
	* если игрок находится в игре, либо убирает игрока из очереди и возвращает `true`,
	* если игрок не находится в игре.
	* @param {Player} player
	*
	* @return {boolean} Возвращает нужно ли удалить игрока.
	*/
	disconnectPlayer(player){
		let game = player.game;
		let queue = player.queue;
		// Если игрок не находится в игре, удаляем его
		if(!game){
			if(queue){
				queue.removePlayer(player);
			}
			return true;
		}
		// иначе устанавливаем отключенный статус
		else{
			player.connected = false;
			player.disconnectTime = Date.now();
			return false;
		}
	}

	/**
	* Удаляет игрока из игры.
	* @param  {Player} player
	*/
	concedePlayer(player){
		let queue = player.queue;
		if(queue){
			queue.concedePlayer(player);
		}
		else{
			this.log.notice(`Player has no queue ${player.id}`);
		}
	}

	/**
	* Удаляет игрока из очереди.
	* @param  {Player} player
	*/
	removePlayerFromQueue(player){
		let queue = player.queue;
		if(queue){
			queue.removePlayer(player);
		}
		else{			
			this.log.notice(`Player has no queue ${player.id}`);
		}
	}

	freePlayer(player){
		let playerStatus = this.playerIsBusy(player, true);
		if(playerStatus === QueueManager.PLAYER_STATUS.IN_GAME){
			this.concedePlayer(player);
		}
		else if(playerStatus === QueueManager.PLAYER_STATUS.IN_QUEUE){
			this.removePlayerFromQueue(player);
		}
	}

	/**
	* Возвращает статус игрока.
	* @param  {Player} player 
	* @param {boolean} [silent=false] убирает вывод статуса в консоль
	* @return {QueueManager.PLAYER_STATUS}        
	*/	
	playerIsBusy(player, silent = false){
		if(player.game){
			if(!silent){
				this.log.notice('Player %s already in game %s', player.id, player.game.id);
			}
			return QueueManager.PLAYER_STATUS.IN_GAME;
		}
		if(player.queue){
			if(!silent){
				this.log.notice('Player %s already in queue', player.id, player.queue.id);
			}
			return QueueManager.PLAYER_STATUS.IN_QUEUE;
		}
		return QueueManager.PLAYER_STATUS.FREE;
	}

	/**
	* Переподсоединяет игрока к игре.
	* @param  {Player} player
	*/
	reconnectPlayer(player){
		player.connected = true;
		player.disconnectTime = -1;
		if(player.game){
			player.game.players.reconnect(player);
		}
	}
} 

/**
* Статус игрока.
* @enum {number}
*/
QueueManager.PLAYER_STATUS = {
	/** Игрок свободен. */
	FREE: 0,

	/** Игрок в очереди. */
	IN_QUEUE: 1,

	/** Игрок в игре. */
	IN_GAME: 2
};

/**
* {@link QueueManager}
* @module
*/
module.exports = QueueManager;