serverjs/Server/Server.js

'use strict';

// Node модули
const
	express = require('express'),
	http = require('http'),
	path = require('path'),
	Eureca = require('../vendor/eureca.io/EurecaServer'),
	minimist = require('minimist'),
	Log = require('../logger');

// Игровые модули
const
	QueueManager = reqfromroot('Queue/QueueManager'),
	DurakGame = reqfromroot('Game/Durak/DurakGame'),
	Bot = reqfromroot('Player/Bot'),
	Player = reqfromroot('Player/Player'),
	Tests = reqfromroot('Tests/GameTest'),
	getRemoteFunctions = reqfromroot('Server/remoteFunctions');

class Server extends Eureca.Eureca.Server{

	/**
 	 * Сервер на основе eureca.io
	* @param  {object} config    конфигурация Eureca.io сервера
	* @param  {array} paramLine  параметры командной строки
	*/
	constructor(config, paramLine){
		super(config);

		/**
		* Параметры сервера.
		* @type {object}
		*/
		this.params = this.parseParams(paramLine);

		/**
		* Логгер сервера.
		* @type {winston.Logger}
		*/
		this.log = this.createLogger();

		/**
		* Существующие режимы игры в виде `[GameClass, BotClass]`.
		* @type {Object<array>}
		*/
		this.gameModes = {
			'durak': [DurakGame, Bot]
		};

		/**
		* Менеджер очередей и игр.
		* @type {QueueManager}
		*/
		this.manager = new QueueManager(this, {
			game: this.gameModes['durak'][0],
			bot: this.gameModes['durak'][1],
			numPlayers: this.params.numPlayers,
			numBots: this.params.numBots,
			debug: this.params.debug,
			name: 'Quick Game'
		},
		{
			canTransfer: this.params.transfer,
			limitFollowup: !this.params.followup,
			limitAttack: !this.params.attack,
			freeForAll: this.params.freeForAll
		});

		let rootPath = '/../../';
		/**
		* Express приложение.
		* @see {@link https://expressjs.com/}
		* @type {function}
		*/
		this.app = express();
		this.app.set('trust proxy', true);

		if (!process.env.PROD) {
			this.app.use(express.static(path.join(__dirname, rootPath, '/public')));
		}

		/**
		* Подключенные клиенты
		* @type {Object<Object>}
		*/
		this.clients = {};

		/**
		* Игроки.
		* @type {Object<Player>}
		*/
		this.players = {};

		this.playerNames = [];

		for(let i = 0; i < 1000; i++){
			this.playerNames.unshift('Player' + (i + 1));
		}

		// Биндим функции на ивенты
		this.on('connect', this.handleConnect);
		this.on('disconnect', this.handleDisconnect);
		this.on('error', this.handleError);
		this.on('message', this.handleMessage);

		/**
		* Функции, доступные со стороны клиента.
		* @see {@link module:serverjs/Server/remoteFunctions|remoteFunctions}
		* @type {object}
		*/
		this.exports = getRemoteFunctions(this);

		/**
		* Node.js http сервер.
		* {@link Server#app} прикрепляется сюда, как колбэк.
		* http сервер затем прикрепляется к Eureca.Server (расширением которого является текущий класс)
		* и обрабатывается им.
		* @type {http.Server}
		*/
		this.httpServer = http.createServer(this.app);
		this.attach(this.httpServer);
	}

	/**
	* Обрабатывает параметры при помощи minimist.
	* @param  {array} paramLine параметры командной строки
	* @return {object}          Обработанные параметры.
	*/
	parseParams(paramLine){
		let argv = minimist(paramLine);
		let params = {
			numBots: Number(process.env.BOTS) || (argv.b === undefined ? Number(argv.bots) : Number(argv.b)),
			numPlayers: Number(process.env.PLAYERS) || (argv.p === undefined ? Number(argv.players) : Number(argv.p)),
			transfer: Boolean(process.env.TRANSFER || argv.transfer),
			followup: Boolean(process.env.FOLLOWUP || argv.followup),
			freeForAll: Boolean(process.env.FREEFORALL || argv.ffa || argv.freeforall),
			attack: Boolean(process.env.ATTACK || argv.attack),
			testing: argv.t || argv.test || argv.testing || false,
			decisionTime: typeof argv.dt == 'number' ? argv.dt : argv.decisiontime,
			debug: process.env.DEBUG || argv.d || argv.debug || 'notice',
			port: process.env.PORT || Number(argv.port)
		};

		if(params.debug && typeof params.debug != 'string'){
			params.debug = 'debug';
		}

		if(isNaN(params.port) || !params.port){
			params.port = 5000;
		}
		if(isNaN(params.numBots)){
			params.numBots = 0;
		}
		if(isNaN(params.numPlayers) || !params.numPlayers){
			params.numPlayers = 4;
		}
		if(isNaN(params.decisionTime)){
			params.decisionTime = 1500;
		}

		return params;
	}

	/**
	* Создает winston логгер.
	* @return {winston.Logger} Логгер.
	*/
	createLogger(){
		let log = Log(module, null, this.params.debug);

		log.notice(
			'port=' + this.params.port,
			'numBots=' + this.params.numBots,
			'numPlayers=' + this.params.numPlayers,
			'transfer=' + this.params.transfer,
			'freeForAll=' + this.params.freeForAll,
			'decisionTime=' + this.params.decisionTime,
			'testing=' + this.params.testing,
			'debug=' + this.params.debug
		);

		return log;
	}

	/**
	* Обработка подключения нового клиента.
	* @param  {object} conn информация о соединении
	*/
	handleConnect(conn){
		if(this.params.testing){
			return;
		}

		// getClient позволяет нам получить доступ к функциям на стороне клиента
		let remote = this.getClient(conn.id);

		// Запоминаем информацию о клиенте
		this.clients[conn.id] = {id: conn.id, remote: remote};

		let name;
		if(this.playerNames.length){
			name = this.playerNames.pop();
		}

		// Подключаем клиента к экземпляру игрока
		let p = new Player(remote, conn.id, name);

		this.log.notice('New client %s (%s)', p.id, conn.id, conn.remoteAddress);

		// Запускаем игру с ботами и игроком
		this.players[conn.id] = p;
	}

	/**
	* Обработка отключения клиента.
	* @param  {object} conn информация о соединении
	*/
	handleDisconnect(conn){
		if(this.params.testing){
			return;
		}

		this.log.notice('Client disconnected ', conn.id);

		let removeId = conn.id;

		let p = this.players[removeId];

		if(p){
			if(this.manager.disconnectPlayer(p)){
				this.deletePlayer(removeId);
			}
		}

		delete this.clients[removeId];
	}

	/** Оработка ошибок. */
	handleError(conn){

	}

	/**
	* Выполняется при любом ответе от клиента.
	* @param  {object} conn информация о соединении
	*/
	handleMessage(conn){

	}

	/** Запускает сервер */
	start(){
		this.httpServer.listen(this.params.port, () => {
			this.log.notice('Running on port', this.params.port);
			if(this.params.testing){
				Tests.runTest(this.params);
			}
		});
	}

	deletePlayer(connId){
		let player = this.players[connId];
		if(player){
			if(!player.nameChanged){
				this.playerNames.push(player.name);
			}
			delete this.players[connId];
		}
	}

	changePlayerName(connId, name){
		let player = this.players[connId];
		if(player){
			if(typeof name != 'string' || name.length < 1 || name.length > 15){
				player.recieveSystemNotification({type: 'NAME_INVALID'});
				return;
			}
			name = name.replace(/[^a-zA-Z0-9 \.,!?{}\(\)\[\]\*$#@%^&\-+=\\\/_<>~"';:|`]/g, '');
			if(name.length < 1 || name.length > 15){
				player.recieveSystemNotification({type: 'NAME_INVALID'});
				return;
			}
			if(!player.nameChanged){
				this.playerNames.push(player.name);
			}
			player.name = name;
			player.nameChanged = true;
			player.recieveSystemNotification({type: 'NAME_CHANGED', name: name});
		}
	}

}

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