GameInfo/GameInfo.js

/**
* Менеджер информации об игроках, игре и игровых действиях.  
* Производит подсветку возможных действий и активирует кнопку действий,
* перенаправляет сыгранные карты на правильные поля и выводит сообщения о состоянии хода. 
* @class
*/
var GameInfo = function(){

	// Чтобы не копировать, все поля инициализируются в ресете
	this.reset();

	/**
	* Действия, которые будут перенаправлены на {@link ActionHandler#actionButton}.
	* @type {string[]}
	*/
	this.buttonActions = ['PASS', 'TAKE'];
};

GameInfo.prototype = {

	/**
	* Убирает всю информацию об игре (игроков, роли, информацию о ходе, свойства игры).  
	*/
	reset: function(){
		/**
		* id игры
		* @type {string}
		*/
		this.gameId = null;
		/**
		* Индекс игры.
		* @type {number}
		*/
		this.gameIndex = -1;

		/**
		* Правила игры.
		* @type {object}
		*/
		this.rules = null;
		/**
		* Находится игра в режиме быстрой симуляции.
		* Если `true`, все запланированные действия будут завершены перед добавлением новых
		* @type {Boolean}
		*/
		this.simulating = false;
		/**
		* Козырная масть.
		* @type {number}
		*/
		this.trumpSuit = null;

		/**
		* Индекс локального игрока в массиве игроков.
		* @type {number}
		*/
		this.pi = null;
		/**
		* id локального игрока.
		* @type {string}
		*/
		this.pid = null;
		/**
		* Информация о локальном игроке.
		* @type {object}
		*/
		this.player = null;
		/**
		* Массив информации об игроках.
		* @type {Object[]}
		*/
		this.players = [];
		/**
		* Информация об игроках по id игроков.
		* @type {Object<Object>}
		*/
		this.playersById = {};

		/**
		* Индекс хода.
		* @type {number}
		*/
		this.turnIndex = -1;
		/**
		* Текущая стадия хода.
		* @type {string}
		*/
		this.turnStage = null;
		/**
		* Текущий защищающийся.
		* @type {object}
		*/
		this.defender = null;
		/**
		* Текущий доминирующий атакующий.
		* @type {object}
		*/
		this.attacker = null;

		if(this.message){
			this._removeMessage(this.message);
		}
		/**
		* Сообщение о текущем состоянии хода.
		* @type {Phaser.Text}
		*/
		this.message = null;
	},

	/**
	* Сохраняет информацию об игре.
	* @param {string}  gameId     id игры
	* @param {number}  gameIndex  индекс игры
	* @param {object}  rules      правила игры
	* @param {boolean} simulating находится ли игра в режиме ускоренной симуляции
	*/
	saveGameInfo: function(gameId, gameIndex, rules, simulating, trumpSuit){
		this.gameId = gameId;
		this.gameIndex = gameIndex;
		this.rules = rules;
		this.simulating = simulating || false;
		this.trumpSuit = trumpSuit;
	},

	/**
	* Сохраняет информацию об игроках.
	* @param  {array} players
	*/
	savePlayers: function(players){
		this.pid = game.pid;
		this.players = players;
		this.playersById = {};
		players.forEach(function(p, i){ 
			this.playersById[p.id] = p;
			if(p.id == this.pid){
				this.player = p;
				this.pi = i;
			}
		}, this);
		if(!~this.pi){
			console.error('Game info: Player', this.pid, 'not found in players\n', players);
		}		
	},

	/**
	* Возвращает информацию об игрока по id.
	* @param {string} pid
	*
	* @return {object}
	*/
	getPlayer: function(pid){
		if(this.playersById[pid]){
			return this.playersById[pid];
		}
		else{
			console.error('Game info: Player', pid, 'not found in players\n', this.players);
			return null;
		}
	},

	/**
	* Обновляет статусы и роли игроков, запоминает текущую стадию хода,
	* и выводит сообщение о состоянии хода. 
	* @param {object}  statuses   статусы игроков
	* @param {number}  turnIndex  номер хода
	* @param {string}  turnStage  стадия хода
	* @param {boolean} hasActions игрок может ходить
	* @param {object}  [seq]      последовательность действий, в которую будет добавлено
	*                             удаление старого сообщения о состоянии хода
	*/
	updateTurnInfo: function(statuses, turnIndex, turnStage, hasActions, seq){

		if(this._shouldResetTurnInfo(turnStage, hasActions)){
			this.resetTurnInfo(seq);
		}

		this.turnStage = turnStage;
		this.turnIndex = turnIndex;

		if(game.inDebugMode && statuses){
			this._logPlayerRoles(statuses);
		}

		this._updatePlayerRoles(statuses);

		this._updateMessage(seq);
	},

	/**
	* Обнуляет роли игроков и стадию хода, удаляет сообщение о статусе хода.
	* @param {object} [seq] последовательность в которую будет добавлено удаление сообщения о состоянии хода
	*/
	resetTurnInfo: function(seq){
		if(this.message){
			this._removeMessage(this.message, seq);
			this.message = null;
		}
		this.turnStage = null;
		this.players.forEach(function(p){
			p.role = null;
			p.roleIndex = null;
		});
		this.attacker = null;
		this.defender = null;
	},

	/**
	* Возвращает нужно ли ресетить информацию о ходе.
	* @param {string}  turnStage  текущая стадия хода
	* @param {boolean} hasActions может ли игрок ходить
	*
	* @return {boolean}
	*/
	_shouldResetTurnInfo: function(turnStage, hasActions){
		return turnStage == 'DEFENSE_TRANSFER' && !hasActions;
	},

	/**
	* Выводит статусы игроков в консоль.
	* @param  {object} statuses
	*/
	_logPlayerRoles: function(statuses){
		console.log('------');
		this.players.forEach(function(p){
			var status = statuses[p.id];
			console.log(p.name, ':', status.role, status.roleIndex, status.working, status.defenseStartCards);
		});
	},

	/**
	* Обновляет роли игроков.
	* @param {object} statuses текущие статусы игроков
	*/
	_updatePlayerRoles: function(statuses){

		if(!statuses){
			this.resetTurnInfo();
			return;
		}

		var playerIsAttacker = this._roleIsAttacker(statuses[this.player.id]);

		this.players.forEach(function(p){
			var status = statuses[p.id];
			var role = status && status.role || null;
			var roleIndex = role && status.roleIndex || null;
			var working = role && status.working || false;
			var defenseStartCards = status.defenseStartCards || 0;
			if(!role){
				if(this.defender == p){
					this.defender = null;
				}
				if(this.attacker == p){
					this.attacker = null;
				}
			}
			else if(role != p.role || roleIndex != p.roleIndex || working != p.working){
				if(role == 'defender' || role == 'takes'){
					this.defender = p;
				}
				else if(
					playerIsAttacker && p == this.player ||
					!playerIsAttacker && this._roleIsAttacker(status)
				){
					this.attacker = p;
				}
			}

			p.role = role;
			p.roleIndex = roleIndex;
			p.working = working;
			p.defenseStartCards = defenseStartCards;
			p.status = status.status;
		}, this);
	},

	/**
	* Определяет, является ли игрок атакуюшим по статусу.
	* @param {object} status
	*
	* @return {boolean}
	*/
	_roleIsAttacker: function(status){
		if(!status){
			return false;
		}
		return status.role == 'attacker' && status.working;
	},

	/**
	* Обновляет сообщение о состоянии хода, удаляет предыдущее сообщение.
	* @param {object} [seq] последовательность, в которую будет добавлено удаление старого сообщения
	*/
	_updateMessage: function(seq){

		var oldMessage = this.message;
		var newMessage = this._getNewMessage();

		if(!oldMessage || oldMessage.text != newMessage.text){
			if(newMessage.text){
				this.message = ui.eventFeed.newMessage(newMessage.text, newMessage.style);
			}
			else{
				this.message = null;
			}
		}
		if(oldMessage && (oldMessage.text != newMessage.text || !this.message)){
			this._removeMessage(oldMessage, seq);
		}
	},

	/**
	* Удаляет сообщение о состоянии хода.
	* @param {Phaser.Text} message
	* @param {object}      [seq]   последовательность, в которую будет добавлено удалени
	*/
	_removeMessage: function(message, seq){
		if(seq){
			seq.append(300).then(ui.eventFeed.removeMessage.bind(ui.eventFeed, message));
		}
		else{
			ui.eventFeed.removeMessage(message);
		}
	},

	/**
	* Возвращает сообщение о состоянии хода.
	* @return {object} `{text, style}`
	*/
	_getNewMessage: function(){
		var player = this.player;

		var messageText = '',
			messageStyle = null;


		if(this.turnStage != 'TAKE' && this.turnStage != 'END' && this.turnStage != 'END_DEAL'){

			var onlyOneAttacker = !this.rules.freeForAll || this.turnStage == 'INITIAL_ATTACK' || this.turnStage == 'ATTACK';

			if(player == this.defender || player == this.attacker){
				switch(player.role){
					case 'attacker':
					if(fieldManager.fields[this.pid].cards.length > 0){
						messageText = (this.defender.role == 'takes' ? 'You\'re following up on ' : 'You\'re attacking ') + this.defender.name;
					}
					messageStyle = 'neutral';
					break;

					case 'defender':
					messageText = 'You\'re defending';
					messageStyle = 'neutral';
					break;

					case 'takes':
					messageText = 'Following up...';
					messageStyle = 'system';
					break;

					default:
					console.error('Game info: unknown player role', player.role);
				}
			}
			else if(this.attacker && onlyOneAttacker){
				var action = this.defender.role == 'takes' ? ' is following up on ' : ' is attacking ';
				var defenderName = this.defender.id == game.pid ? 'you' : this.defender.name;
				messageText = this.attacker.name + action + defenderName;
				messageStyle = 'system';
			}
			else if(this.defender){
				messageText = this.defender.name + ' is defending'; 
				messageStyle = 'system';
			}
		}

		return {
			text: messageText,
			style: messageStyle
		};
	},

	/**
	* Находит поле, на которое можно положиь карту.  
	* Меняет местами `field` с `field.linkedField` и возвращает `linkedField` если оно есть.
	* @param {Field} field поле, над которым находится карта
	*
	* @return {Field} Поле, на которое можно положить карту.
	*/
	findAppropriateField: function(field){
		if(field.linkedField && !field.icon && !field.cards.length){
			fieldManager.swapFields(field, field.linkedField);
			return field.linkedField;
		}
		if(field.type == 'TABLE' && field.playable == 'ATTACK'){
			var emptyTable = fieldManager.getFirstEmptyTable();
			if(emptyTable){
				return emptyTable;
			}
		}
		return field;
	},

	/** Находит кол-ва полей стола с картами */
	getFieldStatuses: function(){
		return {
			numDefenseFields: fieldManager.getFieldsWith(function(f){
					return f.type == 'TABLE' && f.cards.length == 1;
			}).length,

			numAttackFields: fieldManager.getFieldsWith(function(f){
				return f.type == 'TABLE' && f.cards.length > 0;
			}).length,

			firstEmptyTable: fieldManager.getFirstEmptyTable()
		};
	},

	/**
	* Возвращает нужно ли удалить действие в соответствии с типом действия, правилами игры и `turnStage`.  
	* В некоторых случаях модифицирует действие.
	* @param {ActionInfo} action     проверяемое действий
	* @param {Card}       card       использованная в `doneAction` карта
	* @param {Field}      field      использованное в `doneAction` поле
	* @param {ActionInfo} doneAction исполненное действие
	*
	* @return {boolean}
	*/
	shouldDeleteAction: function(action, card, field, doneAction, fieldStatuses){

		// Избавляемся от кнопочных действий
		switch(action.type){
			case 'TAKE':
			return doneAction.type == 'ATTACK' || fieldStatuses.numDefenseFields === 0;

			case 'PASS':
			return this.turnStage != 'FOLLOWUP' && (!this.rules.canTransfer || this.turnStage != 'ATTACK');
		}

		// Избавляемся от действий с картой, которой мы походили
		if(card.id === action.cid){
			return true;
		}

		// Избавляемся от действий, специфичных определенным стадиям хода
		switch(this.turnStage){

			case 'ATTACK':
			if(this.defender.defenseStartCards <= fieldStatuses.numDefenseFields){
				return true;
			}
			/* falls through */

			case 'INITIAL_ATTACK':
			if(card.value !== cardManager.cards[action.cid].value){
				return true;
			}
			return this._fixActionField(action, fieldStatuses.firstEmptyTable);

			case 'FOLLOWUP':
			return (
				this.rules.limitFollowup && 
				this.defender.defenseStartCards <= fieldStatuses.numAttackFields
			);			

			case 'ATTACK_DEFENSE':
			if(this.player.role == 'attacker'){
				return this._fixActionField(action, fieldStatuses.firstEmptyTable);
			}
			/* falls through */

			case 'DEFENSE':
			return field.id === action.field;

			case 'DEFENSE_TRANSFER':
			if(doneAction.type == 'ATTACK'){
				return true;
			}
			return field.id === action.field || action.type == 'ATTACK';

			default:
			console.error('ActionHandler: unknown turnStage', this.turnStage);
			return true;
		}
	},

	/**
	* Заменяет поле действия на первое свободное поле если оно есть.
	* @param {ActionInfo} action
	* @param {Field}      emptyTable
	*
	* @return {boolean} Нужно ли удалить действие.
	*/
	_fixActionField: function(action, emptyTable){
		if(emptyTable){
			action.field = emptyTable.id;
			return false;
		}
		return true;
	},

	/**
	* Делает элементы игры интерактивными в соответствии с переданными действиям.
	* @param {ActionInfo[]} actions
	* @param {UI.Button}    button  кнопка, на которую будет навешено действие из {@link GameInfo#buttonActions|buttonActions}.
	*/
	applyInteractivity: function(actions, button){

		fieldManager.resetHighlights();

		var cardHolding = cardControl.card;
		var hasButtonAction = false;

		// Находим все поля, которые нужно защищать
		var defenseFields = [];
		actions.forEach(function(action){
			if(action.type == 'DEFENSE'){
				defenseFields.push(fieldManager.fields[action.field]);
			}
		});

		var emptyTable = fieldManager.getFirstEmptyTable();

		// Обрабатываем действия
		for(var ai = 0; ai < actions.length; ai++){
			var action = actions[ai];

			// Устанавливаем кнопочное действие
			if(~this.buttonActions.indexOf(action.type)){
				hasButtonAction = true;
				this._setButtonAction(button, action.type);
				continue;
			}

			var card = cardManager.cards[action.cid];
			var field = fieldManager.fields[action.field];

			// Подсвечиваем карту и поле
			if(this._cardShouldBePlayable(card, action, cardHolding)){
				this._makeInteractible(card, field, action, defenseFields, emptyTable);
			}
		}

		// Подсвечиваем кнопку, если это единственное действие, или ресетим ее
		if(hasButtonAction){
			button.changeStyle(actions.length == 1 && gameOptions.get('ui_glow') ? 1 : 0);
		}
		else{
			this._resetButton(button);
		}

		// Подсвечиваем dummy поле
		this.tryHighlightDummy(emptyTable);
	},

	/**
	* Подсвечивает dummy поле, если все поля стола играбильны.
	* @param  {TableField} emptyTable первое пустое поле ввода
	*/
	tryHighlightDummy: function(emptyTable){

		// Мы подсвечиваем поле, если включен hard mode или если нет ни одного стола,
		// на который нельзя играть карты
		var allMarked = !gameOptions.get('ui_glow') || fieldManager.getFieldsWith(function(f){
			return f.type == 'TABLE' && !f.playable;
		}).length === 0;

		if(!allMarked){
			return;
		}

		// Убираем подсветку полей, на которые можно атаковать
		fieldManager.forEachField(function(f){
			if(f.playable != 'ATTACK'){
				return;
			}
			f.setOwnHighlight(false);
			f.setIconVisibility(true);
		});

		// Подсвечиваем поле, если выключен hard mode или если игрок атакует или может перевести,
		// и при этом есть пустое поле и карты в руках
		if(
			gameOptions.get('ui_glow') || 
			(this.attacker == this.player || this.turnStage == 'DEFENSE_TRANSFER' && this.defender == this.player) && 
			emptyTable && 
			fieldManager.fields[this.pid].length > 0
		){
			fieldManager.fields.dummy.setOwnHighlight(true);
		}
	},

	/** 
	* Возвращает нужно ли сделать карту играбильной.
	* @param {Card}       card
	* @param {ActionInfo} actions
	* @param {Card}       cardHolding карта, которую держит игрок
	*
	* @return {boolean}
	*/
	_cardShouldBePlayable: function(card, action, cardHolding){
		return action.cid && card && (!cardHolding || (cardHolding == card || cardHolding.value == card.value && action.type != 'DEFENSE'));
	},

	/** 
	* Делает поле и карту интерактивной в соответствии с действием.
	* @param {Card}       card
	* @param {Field}      field
	* @param {ActionInfo} action
	* @param {Field[]}    defenseFields поля, которые игрок должен отбивать
	* @param {Field}      emptyTable    первое свободное поле на столе
	*/
	_makeInteractible: function(card, field, action, defenseFields, emptyTable){
		card.setPlayability(true);
		field.setOwnPlayability(action.type);
		switch(action.type){
			case 'DEFENSE':
			field.validCards.push(card);				
			break;

			case 'ATTACK':
			if(!emptyTable){
				break;
			}
			fieldManager.table.forEach(function(f){
				if(!~defenseFields.indexOf(f)){
					f.setOwnPlayability(action.type, emptyTable);
				}
			});
			break;		
		}
	},

	/**
	* Ресетит кнопку действия.
	* @param  {UI.Button} button
	*/
	_resetButton: function(button){
		button.serverAction = null;
		button.disable();
		button.changeStyle(0);
		button.label.setText(this.player.role == 'defender' ? 'Take' : 'Pass', true);
	},

	/**
	* Устанавливает текст и действие кнопки действия.
	* @param {UI.Button} button кнопка действия
	* @param {string}    type   тип действия
	*/
	_setButtonAction: function(button, type){
		button.serverAction = type;
		var typeText = type.charAt(0).toUpperCase() + type.slice(1).toLowerCase();
		//var typeText = type.toLowerCase();
		//var typeText = type;
		button.label.setText(typeText, true);
		button.enable();
	},

	/**
	* Возвращается является ли игрок активным в данный момент.
	* @param {string}  pid       id игрока
	* @param {boolean} wasActive был ли игрок активным в предыдущую фазу
	*
	* @return {boolean} активен ли игрок теперь
	*/
	playerIsActive: function(pid, wasActive){
		var player = this.playersById[pid];
		if(!player){
			return false;
		}
		return (
			player.role && player.working || // Текущий атакующий
			player.role == 'defender' || player.role == 'takes' || // Защищающийся
			wasActive && ~['TAKE', 'END', 'END_DEAL'].indexOf(this.turnStage) // Был активным и конец хода
		);
	}
};