'use strict';
const
Log = require('../logger'),
generateId = reqfromroot('generateId');
class Queue{
/**
* Очередь.
* @class
* @param {QueueManager} manager менеджер очередей
* @param {string} type тип очереди
* @param {object} config конфигурация очереди
* @param {object} rules правила игры
*/
constructor(manager, type, config, rules){
/**
* id очереди
* @type {String}
*/
this.id = 'queue_' + generateId();
/**
* Логгер очереди.
* @type {winston.Logger}
*/
this.log = Log(module, this.id, config.debug);
/**
* Активна ли очередь
* (неактивные очереди были удалены из менеджера, но у игроков может остаться на них ссылка).
* @type {Boolean}
*/
this.active = true;
/**
* Запущенная очередью игра.
* @type {Game}
*/
this.game = null;
/**
* Менеджер очереди.
* @type {QueueManager}
*/
this.manager = manager;
/**
* Тип очереди.
* @type {string}
*/
this.type = type;
this.savedType = null;
/**
* Имя очереди.
* @type {string}
*/
this.name = config.name || 'Unnamed Game';
if(
typeof config.numPlayers != 'number' || isNaN(config.numPlayers) ||
config.numPlayers < 1
){
config.numPlayers = config.game.minPlayers;
}
config.numPlayers = Math.min(config.numPlayers, config.game.maxPlayers);
if(
typeof config.numBots != 'number' || isNaN(config.numBots) ||
config.numPlayers + config.numBots > config.game.maxPlayers ||
config.numBots < 0
){
config.numBots = 0;
}
config.addedBots = 0;
/**
* Конфигурация очереди.
* @type {object}
*/
this.config = config;
/**
* Конфигурация игры, запускаемой этой очередью.
* @type {object}
*/
this.gameConfig = {
debug: config.debug
};
/**
* Правила игры очереди.
* @type {object}
*/
this.gameRules = this.config.game.sanitiseRules(rules);
/**
* Игроки в этой очереди.
* @type {Array}
*/
this.players = [];
/**
* Игроки, проголосовавшии за старт с ботами.
* @type {Array}
*/
this.playersReady = [];
this.log.notice('Queue initialized');
}
/**
* Информация об очереди.
* @return {object}
*/
get info(){
return {
id: this.id,
type: this.type,
started: !!this.game,
gameMode: this.config.game.modeName,
numPlayers: this.players.length,
numPlayersRequired: this.config.numPlayers,
numBots: this.config.numBots,
numBotsAdded: this.config.addedBots,
gameRules: this.gameRules,
difficulty: this.config.difficulty,
name: this.name,
playerNames: this.players.map(p => p.name)
};
}
/**
* Добавляет игрока в очередь.
* Оповещает игроков о новом игроке в очереди.
* Запускает очередь, если в ней достаточное кол-во игроков.
* @param {Player} player
*/
addPlayer(player){
if(this.game){
this.log.notice('Can\'t add players when game is in progress');
player.recieveMenuNotification({type: 'QUEUE_FULL'});
return;
}
if(this.inactive){
this.log.notice('Can\'t add players to an inactive queue');
player.recieveMenuNotification({type: 'QUEUE_INACTIVE'});
return;
}
this.log.notice('Player connected', player.id);
this.players.push(player);
player.queue = this;
player.recieveQueueNotification({type: 'QUEUE_ENTERED', qid: this.id});
if(this.players.length >= this.config.numPlayers){
this.startGame();
}
else{
this.notifyPlayers([player], false);
}
}
/**
* Создает и запускает новую игру.
* Оповещает игроков о том, что очередь заполнилась.
*/
startGame(){
if(this.game){
this.log.error(new Error('Can\'t start a game, another one is already in progress'));
return;
}
this.log.notice('Starting game');
this.playersReady.length = 0;
let players = this.players.slice();
let numBots = this.config.numPlayers - players.length + this.config.numBots;
if(numBots > 0 && this.config.bot){
let randomNamesCopy = this.manager.randomNames.slice();
for (let n = 0; n < numBots; n++) {
let bot = this.createBot(randomNamesCopy, n < this.config.addedBots);
players.push(bot);
}
}
this.players.forEach((p) => p.recieveQueueNotification({type: 'QUEUE_READY'}));
this.game = new this.config.game(this, players, this.gameConfig, this.gameRules);
this.manager.games[this.game.id] = this.game;
this.manager.removeQueueFromList(this, true);
this.game.init();
}
/**
* Изменяет настройки очереди, чтобы заполнить пустые места ботами,
* создает и начинает игру.
*/
startGameWithBots(){
this.log.notice('Switching to botmatch');
let numPlayers = this.config.numPlayers;
this.config.addedBots = numPlayers - this.players.length;
this.config.numBots += this.config.addedBots;
this.config.numPlayers = this.players.length;
this.savedType = this.type;
this.type = 'botmatch';
this.startGame();
}
/**
* Удаляет игру и неактивных игроков из очереди по окончании игры.
* Вызывается из игры.
* Запускает новую игру, если в очереди достаточно игроков.
* Удаляет очередь, если в ней не осталось игроков.
* Оповещает игроков о состоянии очереди в остальных случаях.
* @param {array} [voteResults] результаты голосования за рематч
*/
endGame(voteResults){
if(!this.game){
this.log.error(new Error('No game to end'));
return;
}
this.log.notice('Game ended');
let results = {};
let left = [];
if(voteResults){
voteResults.forEach(r => results[r.pid] = r.type);
}
for(let i = this.players.length - 1; i >= 0; i--){
let p = this.players[i];
this.log.debug(p.id, 'voted', results[p.id] || 'no vote');
if(!p.connected || !results[p.id] || results[p.id] != 'ACCEPT'){
left.push(p);
this.removePlayer(p, false);
}
}
delete this.manager[this.game.id];
this.game = null;
if(this.savedType){
this.type = this.savedType;
this.savedType = null;
this.config.numBots -= this.config.addedBots;
this.config.numPlayers += this.config.addedBots;
this.config.addedBots = 0;
}
if(!this.players.length){
this.shutdown();
}
else if(this.players.length >= this.config.numPlayers){
this.startGame();
}
else{
this.manager.addQueueToList(this);
this.players.forEach((p) => p.recieveQueueNotification({type: 'QUEUE_ENTERED', qid: this.id}));
this.notifyPlayers(left, true);
}
}
/**
* Прерывает игру, удаляет всех игроков и удаляет очередь из менеджера.
*/
shutdown(){
if(!this.active){
return;
}
this.log.notice('Shutting down');
if(this.players.length){
for(let i = this.players.length - 1; i >= 0; i--){
this.removePlayer(this.players[i], false, true);
}
}
if(this.game){
this.game.shutdown();
}
this.active = false;
this.manager.removeQueue(this);
}
createBot(randomNames, replacement){
return new this.config.bot(randomNames, this.config.decisionTime, this.config.difficulty, replacement)
}
voteForPrematureStart(player){
if(this.game || !~this.players.indexOf(player) || ~this.playersReady.indexOf(player)){
return;
}
this.playersReady.push(player);
this.players.forEach((p) => {
if(p != player){
p.recieveQueueNotification({type: 'QUEUE_READY_VOTE', name: player.name});
}
});
if(this.playersReady.length == this.players.length){
this.startGameWithBots();
}
}
/**
* Сообщает игрокам о состоянии очереди.
*/
notifyPlayers(players, left = false){
if(this.game){
return;
}
this.players.forEach((p) => {
let names = [];
if(players){
players.forEach((pp) => {
if(pp.id != p.id){
names.push(pp.name);
}
});
}
p.recieveQueueNotification({
type: 'QUEUE_STATUS',
playersQueued: this.players.length,
playersNeeded: this.config.numPlayers,
names: names,
left: left
});
});
this.log.notice('Waiting for players:', this.config.numPlayers - this.players.length);
}
/**
* Удаляет игрока из очереди.
* Оповещает игроков об удаленном игроке.
* Удаляет очередь, если в ней не осталось игроков.
* @param {Player} player
* @param {Boolean} notify нужно ли оповещать игроков об удалении игрока из очереди
* @param {Boolean} alreadyShuttingDown отменяет остановку очереди, которая может произойти, если в ней не осталось игроков
*/
removePlayer(player, notify = true, alreadyShuttingDown = false){
if(player.game){
this.log.warn('Cannot remove player in a game from queue', player.id, player.game.id);
return;
}
if(!this.players.includes(player)){
this.log.error(new Error(`Player isn't in this queue ${player.id}`));
return;
}
let i = this.playersReady.indexOf(player);
if(~i){
this.playersReady.splice(i, 1);
}
i = this.players.indexOf(player);
this.players.splice(i, 1);
player.queue = null;
if(notify){
player.recieveQueueNotification({type: 'QUEUE_LEFT', instant: true});
this.notifyPlayers([player], true);
}
this.log.notice('Player %s left queue', player.id, this.id);
if(!this.players.length && !alreadyShuttingDown){
this.shutdown();
}
else if(this.players.length > 0 && this.playersReady.length == this.players.length){
this.startGameWithBots();
}
}
/**
* Если игра запущена, убирает игрока из игры и очереди.
* @param {Player} player
*/
concedePlayer(player){
if(this.game && this.game.isRunning){
this.game.players.concede(player);
this.removePlayer(player, false);
}
else{
this.log.notice('Player %s isn\'t in a game or game has ended, cannot concede', player.id);
}
}
}
/**
* {@link Queue}
* @module
*/
module.exports = Queue;