├── .gitattributes ├── .gitignore ├── README.md ├── index.js ├── lib ├── admin_manager.js ├── chat_manager.js ├── conf.js ├── develop │ └── storage_interface.js ├── engine.js ├── game_manager.js ├── history_manager.js ├── invite_manager.js ├── logger.js ├── mongo.js ├── rating_manager.js ├── room.js ├── router.js ├── server.js ├── storage_interface.js └── user.js ├── package.json └── test ├── app.js ├── conf.js └── engine.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # ========================= 18 | # Operating System Files 19 | # ========================= 20 | 21 | # OSX 22 | # ========================= 23 | 24 | .DS_Store 25 | .AppleDouble 26 | .LSOverride 27 | 28 | # Icon must end with two \r 29 | Icon 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | 45 | .idea/* 46 | node_modules/* 47 | npm-debug.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # V6-Game-Server 2 | 3 | Сервер на node.js для пошаговых игр. 4 | 5 | ## Установка 6 | 7 | npm install v6-game-server 8 | 9 | ## Запуск 10 | 11 | ```js 12 | var Server = require('v6-game-server', 13 | // настройки 14 | conf = {}, 15 | // игровой движок 16 | engine = {}, 17 | // сервер 18 | server = new Server(conf, engine); 19 | 20 | server.start(); 21 | ``` 22 | 23 | ## Настройки 24 | Настройки сервера с параметрами по умолчанию 25 | 26 | ```js 27 | { 28 | game: 'default', // обязательный парамерт, алиас игры и бд 29 | port: 8080, // порт подключения веб сокета 30 | pingTimeout:100000, // таймаут клиента в мс 31 | pingInterval:10000, // интервал пинга клиента в мс 32 | closeOldConnection: true, // закрывать старое соединение клиента, при открытии нового 33 | loseOnLeave: false, // засчитывать поражение при разрыве соединения с клиентом 34 | reconnectOldGame: true, // загружать игру клинета, в которой произошел разрыв 35 | spectateEnable: true, // разрешить просмотр игр 36 | logLevel:3, // уровень подробности лога, 0 - без лога, 1 - только ошибки 37 | turnTime: 100, // время на ход игрока в секундах 38 | timeMode: 'reset_every_switch', // режимы таймера: 39 | // 'reset_every_turn' сбрасывать после каждого хода 40 | // 'reset_every_switch' сбрасывать после перехода хода 41 | // 'dont_reset' не сбрасывать таймер, время на всю партию 42 | // 'common' у игроков общее время на ход 43 | timeStartMode: 'after_switch', // когда запускать таймер 44 | // 'after_turn' после первого хода 45 | // 'after_switch' после первого перехода хода 46 | // 'after_round_start' сразу после начала раунда 47 | addTime: 0, // сколько милисекунд добавлять к времени на ход игрока после каждого его хода 48 | maxTimeouts: 1, // разрешенное число пропусков хода игрока подряд до поражения 49 | clearTimeouts: true, // обнулять число пропусков игрока после его хода 50 | maxOfflineTimeouts: 1, // число пропусков отключенного игрока подряд до поражения 51 | minTurns: 0, // минимальное число число ходов (переходов хода) для сохранения игры 52 | takeBacks: 0, // число разрешенных игроку ходов назад 53 | loadRanksInRating: false, // загружать актуальные ранги при открытии таблицы рейтинга 54 | ratingUpdateInterval: 1000, // интервал обновления рангов в списке игроков 55 | penalties: false, // загружать штарфы игроков 56 | mode: 'debug', // значение 'develop' уставновит режим без использования бд 57 | gameModes: ['default'], // игровые режимы, со своим рейтингом и историей, имена без пробелов 58 | modesAlias:{default:'default'}, // отображаемые клиенту алиасы режимов 59 | enableIpGames: false, // разрешает игры с одного ip 60 | minUnfocusedTurns: 0, // минимальное число ходов с потерей фокуса для засчитывания победы как читерской 61 | // 0 - не считать 62 | minPerUnfocusedTurns: 0.9, // соотношение числа ходов с потерей фокуса для засчитывания победы как читерской 63 | adminList: [], // список userId админов 64 | adminPass: '', // пароль для функций администратора 65 | mongo:{ // настройки подключения mongodb 66 | host: '127.0.0.1', 67 | port: '27017' 68 | }, 69 | redis:{ // настройки подключения redis 70 | host: '127.0.0.1', 71 | port: '6379' 72 | }, 73 | https: true, // настройки https 74 | httpsKey: '/path../serv.key', 75 | httpsCert: '/path../serv.crt', 76 | httpsCa: ['/path../sub.class1.server.ca.pem', '/path../ca.pem'], 77 | } 78 | ``` 79 | 80 | Примеры настроек: 81 | - обычная игра, время на ход 30 секунд, разрешен один пропуск хода, 82 | время игрока обнуляется после каждого хода, таймер стартует после первого хода 83 | ```js 84 | { 85 | turnTime: 30, 86 | maxTimeouts: 2, 87 | timeMode: 'reset_every_turn', 88 | timeStartMode: 'after_turn', 89 | } 90 | ``` 91 | 92 | - блиц, время на партию 60 секунд, время игрока не обнуляется, 93 | после каждого его хода к его времени на ход добавляется 1 секунда, 94 | таймер стартует после перехода хода к другому игроку, 95 | после первого пропуска хода ему засчитывается поражение 96 | ```js 97 | { 98 | turnTime: 60, 99 | maxTimeouts: 1, 100 | timeMode: 'dont_reset', 101 | timeStartMode: 'after_switch', 102 | addTime: 1000 103 | } 104 | ``` 105 | 106 | - игра с общим временем на ход, по типу "кто быстрее", 107 | таймер страртует сразу после начала раунда, 108 | по истечении часа срабатывает таймаут и необходимо решить результат игры 109 | ```js 110 | { 111 | turnTime: 3600, 112 | maxTimeouts: 2, 113 | timeMode: 'common', 114 | timeStartMode: 'after_round_start', 115 | } 116 | ``` 117 | 118 | ## Игровой движок 119 | Методы игрового движка 120 | 121 | ``` js 122 | { 123 | /** 124 | * вызывается после соединения нового пользователя в первый раз 125 | * устанавливает значения нового пользователя по умолчанию 126 | * рейтинги, очки, и т.д. 127 | */ 128 | initUserData: function(mode, modeData){ 129 | return modeData; 130 | }, 131 | 132 | /** 133 | * вызывается перед началом игрового раунда 134 | * возвращаемый объект будет передан всем игрокам в начале раунда 135 | * по умолчанию возвращает объект переданный игроком в приглашении 136 | */ 137 | initGame: function (room) { 138 | return { 139 | inviteData: room.inviteData 140 | } 141 | }, 142 | 143 | /** 144 | * вызывается в начале раунда 145 | * возвращает игрока, который будет ходить первым 146 | * по умолчанию первым ходит создатель комнаты, 147 | * в следующем раунде ход переходит к другому игроку 148 | */ 149 | setFirst: function (room) { 150 | if (!room.game.first) return room.owner; 151 | return room.getOpponent(room.game.first) 152 | }, 153 | 154 | /** 155 | * вызывается каждый ход или пропуск хода игрока 156 | * возвращаемый объект будет передан всем игрокам и записан в историю 157 | * если вернуть false || null || undefined ход будет признан некорректным 158 | * в случае пропуска хода, turn = {action: 'timeout'} 159 | * если вернуть объект с полем action = 'timeout' 160 | * он будет принят как событие пропуск хода, иначе как обычный ход 161 | * type {'turn'|'timeout'} - ход игрока или таймаут 162 | */ 163 | doTurn: function(room, user, turn, type){ 164 | if (type == 'timeout'){ 165 | // this is user timeout 166 | } 167 | return turn; 168 | }, 169 | 170 | /** 171 | * вызывается каждый ход игрока или после события пропуска хода 172 | * возвращаемый игрок будет ходить следующим 173 | * если вернуть того же игрока, чей был ход, ход останется за ним 174 | * type {'turn'|'timeout'} - ход игрока или таймаут 175 | */ 176 | switchPlayer: function(room, user, turn, type){ 177 | if (type == 'timeout'){ 178 | // this is user timeout 179 | } 180 | return room.getOpponent(user); 181 | }, 182 | 183 | /** 184 | * вызывается после отправке игроком события 185 | * возвращаемый объект будет передан заданным игрокам, и должен быть следующего вида: 186 | * { event, target, user } || [Array], где 187 | * event - объект с обязательным полем type 188 | * target - цель для отправки события null || Room || User 189 | * может быть массивом с разными объектами событий и целями 190 | */ 191 | userEvent: function(room, user, event){ 192 | return { 193 | event: event, 194 | target: room, 195 | user: user.userId 196 | } 197 | }, 198 | 199 | /** 200 | * вызывается в начале раунда и после каждого хода игрока 201 | * возвращаемый объект будет передан заданным игрокам, и должен быть следующего вида: 202 | * { event, target, user } || [Array], где 203 | * event - объект с обязательным полем type 204 | * target - цель для отправки события null || Room || User 205 | * может быть массивом с разными объектами событий и целями 206 | */ 207 | gameEvent: function(room, user, turn, roundStart){ 208 | return null; 209 | }, 210 | 211 | /** 212 | * вызывается каждый ход и событие, определяет окончание раунда 213 | * возвращаемый объект будет передан всем игрокам 214 | * и должен быть вида {winner : user}, где 215 | * user - User (игрок победитель ) || null (ничья) 216 | * если вернуть false - раунд еще не окончен 217 | * дополнительное поле 'action' указывает на действие, 218 | * по которому завершилась игра, по умолчанию 'game_over' 219 | * если пользователь не подключен, то игра завешится по 220 | * максимальному числу офлайн таймаутов 221 | * если не подключены оба, завершится поражением пропустившего 222 | * если не обрабатывать пропускать ход можно бесконечно 223 | * type {'turn'|'event','timeout'} - ход, событие или таймаут 224 | */ 225 | getGameResult: function(room, user, turn, type){ 226 | switch (type){ 227 | case 'timeout': 228 | if (type == 'timeout'){ 229 | // if user have max timeouts, other win 230 | if (room.data[user.userId].timeouts == room.maxTimeouts){ 231 | return { 232 | winner: room.getOpponent(user), 233 | action: 'timeout' 234 | }; 235 | } else return false; 236 | } 237 | break; 238 | case 'event': 239 | if (turn.type){ 240 | return false; 241 | } 242 | break; 243 | case 'turn': 244 | switch (turn.result){ 245 | case 0: // win other player 246 | return { 247 | winner: room.getOpponent(user) 248 | }; 249 | break; 250 | case 1: // win current player 251 | return { 252 | winner: user 253 | }; 254 | break; 255 | case 2: // draw 256 | return { 257 | winner: null 258 | }; 259 | break; 260 | default: // game isn't end 261 | return false; 262 | } 263 | break; 264 | } 265 | }, 266 | 267 | /** 268 | * вызывается по окончанию раунда 269 | * возвращаемый объект утсанавливает значение очков игроков 270 | * room.players[0][room.mode].['score'] = new_score 271 | */ 272 | getUsersScores: function(room, result){ 273 | // например 274 | for (var i = 0; i < room.players.length; i++){ 275 | if (room.players[i] == result.winner) 276 | room.players[i][room.mode].score += 10; 277 | else room.players[i][room.mode].score -= 10; 278 | } 279 | return result; 280 | }, 281 | 282 | /** 283 | * вызывается после авторизации пользователя 284 | * проверяет подлинноть подписи 285 | */ 286 | checkSign: function(user){ 287 | return (user.userId && user.userName && user.sign); 288 | } 289 | 290 | /** 291 | * действие по вызову администратора 292 | * @param admin 293 | * @param type 294 | * @param data 295 | */ 296 | adminAction: function(admin, type, data){ 297 | 298 | } 299 | }; 300 | ``` 301 | 302 | ## Игровые сущности 303 | 304 | Room 305 | 306 | ``` js 307 | { 308 | owner: User, // создатель 309 | players: Array, // массив с игроками 310 | spectators: Array, // массив зрителей 311 | inviteData: Object // объект приглашения 312 | mode: String // режим 313 | games: Int; // сыграно раундов 314 | turnTime: Int; // время на ход 315 | game: { 316 | state: String // состояние игры: waiting, playing, end 317 | current: User, // текущий игрок 318 | first: User, // игрок, чей ход первый, установленный функцией engine.setFirst 319 | history: Array, // массив с иторией ходов и событий 320 | shistory: String// массив с историей, преобразованный в строку 321 | turnStartTime: UTC // дата начала хода игрока 322 | }, 323 | data: Object, // массив ключ значение, где ключи - userId 324 | // для хранения временной информации для каждого игрока 325 | getOpponent: Function(user: User) // возвращает соперника игрока 326 | setUserTurnTime: Function(time: ms) // устанавливает время на следующий ход 327 | } 328 | ``` 329 | 330 | User 331 | 332 | ``` js 333 | { 334 | userId: String, // идетификатор игрока 335 | userName: String // имя 336 | sign: String // подпись 337 | currentRoom: Room // текущая комната (играет или зритель) 338 | } 339 | ``` 340 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/server.js'); 2 | -------------------------------------------------------------------------------- /lib/admin_manager.js: -------------------------------------------------------------------------------- 1 | var logger = require('./logger.js'); 2 | 3 | module.exports = AdminManager; 4 | 5 | function AdminManager(server){ 6 | this.server = server; 7 | } 8 | 9 | 10 | AdminManager.prototype.onMessage = function(message, type){ 11 | var data = message.data; 12 | var pass = data.pass; 13 | var admin = message.sender; 14 | if (!this.checkAuth(admin, pass)) return; 15 | switch (type){ 16 | case 'reboot': 17 | this.reboot(admin, data.data); 18 | break; 19 | case 'log_level': 20 | this.setLogLevel(admin, data.data); 21 | break; 22 | case 'message': 23 | this.sendMessage(admin, data.data); 24 | break; 25 | case 'enable_games': 26 | this.enableGames(admin, data.data); 27 | break; 28 | case 'reload': 29 | this.reloadClients(admin, data.data); 30 | break; 31 | case 'get_config': 32 | this.getConfig(admin, data.data); 33 | break; 34 | case 'set_config': 35 | this.setConfig(admin, data.data['property'], data.data['value']); 36 | break; 37 | default: 38 | logger.warn('AdminManager', type, admin.userId, admin.userName, 1); 39 | if (typeof this.server.engine.adminAction == "function"){ 40 | try{ 41 | this.server.engine.adminAction(admin, type, data); 42 | } catch (e){ 43 | logger.err('AdminManager, error', type, admin.userId, admin.userName, e, 1); 44 | } 45 | } 46 | } 47 | }; 48 | 49 | AdminManager.prototype.checkAuth = function(user, pass){ 50 | user.wrongAuthAttempts = user.wrongAuthAttempts || 0; 51 | 52 | if (pass !== this.server.conf.adminPass){ 53 | logger.warn('AdminManager.checkAuth', 'wrong pass', user.userId, user.userName, pass, 1); 54 | user.wrongAuthAttempts++; 55 | return false 56 | } 57 | 58 | if (user.wrongAuthAttempts > 2) { 59 | logger.warn('AdminManager.checkAuth', 'auth attempts limit', user.userId, user.userName, 1); 60 | return false; 61 | } 62 | 63 | return true; 64 | }; 65 | 66 | 67 | AdminManager.prototype.reboot = function(user) { 68 | logger.warn('AdminManager.reboot', 'reboot game server', user.userId, user.userName, 1); 69 | process.exit(1); 70 | }; 71 | 72 | 73 | AdminManager.prototype.setLogLevel = function(user, level) { 74 | level = +level; 75 | if (!level && level !== 0) return; 76 | logger.warn('AdminManager.setLogLevel', user.userId, user.userName, level, 1); 77 | logger.logLevel = level; 78 | }; 79 | 80 | 81 | AdminManager.prototype.sendMessage = function(user, message) { 82 | logger.warn('AdminManager.sendMessage', 'message', user.userId, user.userName, message, 1); 83 | if (typeof message != 'string') return; 84 | this.server.router.send({ 85 | module: 'admin', 86 | type: 'message', 87 | target: this.server.game, 88 | data: message 89 | }) 90 | }; 91 | 92 | 93 | AdminManager.prototype.enableGames = function(user, flag) { 94 | flag = !!flag; 95 | logger.warn('AdminManager.enableGames', user.userId, user.userName, flag, 1); 96 | this.server.router.send({ 97 | module: 'admin', 98 | type: 'enable_games', 99 | target: this.server.game, 100 | data: {flag: flag} 101 | }); 102 | this.server.gameManager.enableGames = flag; 103 | }; 104 | 105 | 106 | AdminManager.prototype.reloadClients = function(user) { 107 | logger.warn('AdminManager.reloadClients', user.userId, user.userName, 1); 108 | this.server.router.send({ 109 | module: 'admin', 110 | type: 'reload', 111 | target: this.server.game, 112 | data: true 113 | }); 114 | }; 115 | 116 | 117 | AdminManager.prototype.getConfig = function(user) { 118 | logger.warn('AdminManager.getConfig', user.userId, user.userName, 1); 119 | this.server.router.send({ 120 | module: 'admin', 121 | type: 'get_config', 122 | target: user, 123 | data: this.server.conf 124 | }); 125 | }; 126 | 127 | 128 | AdminManager.prototype.setConfig = function(user, property, value) { 129 | logger.warn('AdminManager.setConfig', user.userId, user.userName, property, value, 1); 130 | if (!property || value == null || value == undefined) return; 131 | if (this.server.conf.hasOwnProperty(property)){ 132 | this.server.conf[property] = value; 133 | } 134 | }; 135 | 136 | -------------------------------------------------------------------------------- /lib/chat_manager.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var logger = require('./logger.js'); 3 | 4 | module.exports = ChatManager; 5 | 6 | function ChatManager(server){ 7 | this.server = server; 8 | this.default = server.game; 9 | this.MESSAGES_INTERVBAL = 1500; 10 | } 11 | 12 | 13 | ChatManager.prototype.onMessage = function(message, type){ 14 | var data = message.data; 15 | switch (type){ 16 | case 'message': 17 | if (data.admin && !message.sender.isAdmin){ 18 | logger.warn('ChatManager.sendMessage', 'try send message not admin ', 19 | message.sender.userId, message.sender.userName, data.text, 1); 20 | return; 21 | } 22 | this.sendMessage(message.sender, data.text, data.target, data.admin); 23 | break; 24 | case 'load': 25 | this.loadMessages(message.sender, data.count, data.time, data.target || this.default, data.type); 26 | break; 27 | case 'ban': 28 | if (message.sender.isAdmin || this.server.conf.mode == 'debug' || this.server.conf.mode == 'develop') 29 | this.banUser(data.userId, data.days, data.reason); 30 | break; 31 | case 'delete': 32 | if (message.sender.isAdmin || this.server.conf.mode == 'debug' || this.server.conf.mode == 'develop') 33 | this.deleteMessage(data.time); 34 | break; 35 | } 36 | }; 37 | 38 | 39 | ChatManager.prototype.sendMessage = function(user, text, target, isAdmin){ 40 | // TODO: check ban user, text length 41 | if (user.isBanned){ 42 | logger.warn('ChatManager.sendMessage', 'user is banned!', user.userId, 1); 43 | return; 44 | } 45 | var time = Date.now(); 46 | if (this.message && this.message.time == time){ 47 | logger.warn('ChatManager.sendMessage', 'fast messages in the same time!', time, 1); 48 | return; 49 | } 50 | if (user.timeLastMessage && time - user.timeLastMessage < this.MESSAGES_INTERVBAL) { 51 | logger.warn('ChatManager.sendMessage', 'double messages in ', user.userId, user.userName, time - user.timeLastMessage, 2); 52 | return; 53 | } 54 | if (text.length > 128) { 55 | logger.warn('ChatManager.sendMessage', 'long messages in ', user.userId, user.userName, text.length, 1); 56 | return; 57 | } 58 | user.timeLastMessage = time; 59 | if (!target) target = this.default; 60 | this.message = { 61 | text: text, 62 | time: time, 63 | userId: user.userId, 64 | userName: (isAdmin?'admin':user.userName), 65 | admin:isAdmin, 66 | target: target, 67 | userData: user.getData(), 68 | type: 'public' 69 | }; 70 | if (target != this.default) { 71 | var targetUser = this.server.storage.getUser(target); 72 | if (targetUser) { // private to user 73 | this.server.router.send({ 74 | module: 'chat_manager', type: 'message', target: user, data: this.message 75 | }); 76 | this.message.type = 'private'; 77 | target = targetUser; 78 | 79 | } else { // message in room 80 | target = this.server.storage.getRoom(target); 81 | this.message.type = 'room' 82 | } 83 | } 84 | this.server.storage.pushMessage(this.message); 85 | if (target) this.server.router.send({ 86 | module:'chat_manager', type: 'message', target: target, data:this.message 87 | }); 88 | }; 89 | 90 | 91 | ChatManager.prototype.loadMessages = function (user, count, time, target, type){ 92 | count = +count; 93 | time = +time; 94 | if (!time) time = Date.now(); 95 | if (!count || count > 100 || count < 0) count = 10; 96 | var self = this; 97 | if (target == this.default) { 98 | type = 'public'; 99 | } 100 | if (type == 'private'){ 101 | type = null 102 | } 103 | this.server.storage.getMessages(count, time, target, type ? null : user.userId) 104 | .then(function (messages) { 105 | self.server.router.send({ 106 | module:'chat_manager', type: 'load', target: user, data:messages 107 | }); 108 | }) 109 | .catch(function (err) { 110 | logger.err('ChatManager.loadMessages', 'can not load messages', err, 1); 111 | self.server.router.send({module:'chat_manager', type: 'load', target: user, data:[]}); 112 | }); 113 | }; 114 | 115 | 116 | ChatManager.prototype.deleteMessage = function (id){ 117 | logger.log('ChatManager.deleteMessage', id, 2); 118 | this.server.storage.deleteMessage(id); 119 | }; 120 | 121 | 122 | ChatManager.prototype.banUser = function (userId, days, reason){ 123 | logger.log('ChatManager.banUser', userId, days, reason, 2); 124 | var timeEnd = Date.now() + days * 1000 * 3600 * 24; 125 | this.server.storage.banUser(userId, timeEnd, reason); 126 | var user = this.server.storage.getUser(userId); 127 | if (user) { 128 | user.isBanned = true; 129 | user.ban = {timeEnd: timeEnd, reason: reason}; 130 | this.server.router.send({ 131 | module: 'chat_manager', type: 'ban', target: user, data: user.ban 132 | }); 133 | } else { 134 | logger.warn('ChatManager.banUser, user not found', userId, 3); 135 | } 136 | }; -------------------------------------------------------------------------------- /lib/conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | game: 'default', // required, game name 3 | port: 8080, // 4 | pingTimeout:60000, // 5 | pingInterval:10000, // 6 | closeOldConnection: true, // 7 | loseOnLeave: false, // player lose game or not after leave 8 | reconnectOldGame: true, // continue old game on reconnect or auto leave 9 | spectateEnable: true, // on/off spectate games 10 | logLevel:4, // 0 - nothing, 1 - errors and warns, 2 - important, 3 and more - others 11 | turnTime: 100, // user turn time in seconds 12 | corTime: 1000, // additional time to turn on server 13 | timeMode: 'reset_every_switch', // time modes, ['reset_every_turn', 'reset_every_switch', 'dont_reset'] 14 | timeStartMode: 'after_switch', // start time, ['after_turn', 'after_switch', 'after_round_start'] 15 | addTime: 0, // add time ms after user turn 16 | maxTimeouts: 1, // count user timeouts in game to lose 17 | clearTimeouts: true, // reset user timeouts after turn 18 | maxOfflineTimeouts: 1, // count offline user timeouts in game to lose 19 | minTurns: 0, // count switches players to save game 20 | takeBacks: 0, // count user take back 21 | ratingElo: true, // compute rating elo flag 22 | calcDraw: false, // compute rating elo after draw 23 | loadRanksInRating: false, // take user ranks in rating table from redis or not 24 | ratingUpdateInterval: 1000, // how often update ranks in users array 25 | penalties: false, // on/off rating penalties 26 | mode: 'debug', // set developing mode, 'develop', without db 27 | gameModes: ['default'], // game modes, with different history, ratings, games, default is one mode ['default'] 28 | modesAlias:{default:'default'}, // visible client mode alias 29 | adminList: [], 30 | adminPass: 'G@adm1n', 31 | enableIpGames: false, // enable play games from one ip 32 | minUnfocusedTurns: 0, // min count cheater tuns without focus 33 | minPerUnfocusedTurns: 0.9, // min percent cheater tuns without focus 34 | mongo:{ // mongodb configuration 35 | host: '127.0.0.1', 36 | port: '27017' 37 | }, 38 | redis:{ 39 | host: '127.0.0.1', 40 | port: '6379' 41 | } 42 | }; -------------------------------------------------------------------------------- /lib/develop/storage_interface.js: -------------------------------------------------------------------------------- 1 | var Promise = require('es6-promise').Promise; 2 | var util = require('util'); 3 | var logger = require('../logger.js'); 4 | 5 | module.exports = StorageInterface; 6 | 7 | function StorageInterface(server){ 8 | this.users = []; 9 | this.allUsers = []; 10 | this.bans = []; 11 | this.rooms = {}; 12 | this.roomsInfo = []; 13 | this.messages = []; 14 | this.lastMessages = []; 15 | this.games = []; 16 | this.history = []; 17 | this.LAST_MSG_COUNT = 10; 18 | this.server = server; 19 | } 20 | 21 | 22 | StorageInterface.prototype.getUserData = function(user){ 23 | logger.log('Develop/StorageInterface.getUserData', user.userId, 3); 24 | var self = this; 25 | return new Promise(function(res, rej){ 26 | var data; 27 | for (var i = 0; i < self.allUsers.length; i++) { 28 | if (self.allUsers[i].userId == user.userId){ 29 | data = self.allUsers[i]; 30 | break; 31 | } 32 | } 33 | data = self.server.initUserData(data); 34 | data.ban = self.checkIsBanned(user.userId); 35 | data.isBanned = data.ban != false; 36 | data.settings = {}; 37 | if (!user.userId || user.userId == 'undefined') rej('user not reg! userId: '+user.userId + ' sessionId: ' + user.sessionId); 38 | res(data); 39 | }); 40 | }; 41 | 42 | StorageInterface.prototype.saveUserSettings = function(user, settings){ 43 | user.settings = settings; 44 | }; 45 | 46 | 47 | //____________ User ___________ 48 | StorageInterface.prototype.pushUser = function(user){ 49 | this.users.push(user); 50 | user.isRemoved = false; 51 | }; 52 | 53 | 54 | StorageInterface.prototype.popUser = function(user){ 55 | var id = (user.userId ? user.userId : user); 56 | for (var i = 0; i < this.users.length; i++){ 57 | if (this.users[i].userId == id) { 58 | this.users[i].isRemoved = true; 59 | this.users.splice(i, 1); 60 | return true; 61 | } 62 | } 63 | logger.err('StorageInterface', "popUser", "user not exists in userlist", user.userId, 1); 64 | return false; 65 | }; 66 | 67 | 68 | StorageInterface.prototype.getUser = function(id){ 69 | for (var i = 0; i < this.users.length; i++){ 70 | if (this.users[i].userId == id) { 71 | return this.users[i]; 72 | } 73 | } 74 | return null; 75 | }; 76 | 77 | 78 | StorageInterface.prototype.getUsers = function(){ 79 | return this.users; 80 | }; 81 | 82 | 83 | StorageInterface.prototype.saveUsers = function(users, mode, callback) { 84 | logger.log('StorageInterface.saveUsers', mode, 3); 85 | // check for duplicate 86 | var user; 87 | for (var j = 0; j < users.length; j++){ 88 | user = users[j]; 89 | var update = false; 90 | for (var i = 0; i < this.allUsers.length; i++) { 91 | if (this.allUsers[i].userId == user.userId){ 92 | logger.log('StorageInterface.saveUser', 'update ', user.userId, 3); 93 | this.allUsers[i] = user.getInfo(); 94 | update = true; 95 | } 96 | } 97 | if (!update) this.allUsers.push(user.getInfo()); 98 | } 99 | if (callback) setTimeout(callback, 100); 100 | }; 101 | 102 | 103 | //____________ Room ___________ 104 | StorageInterface.prototype.pushRoom = function(room){ 105 | this.rooms[room.id] = room; 106 | this.roomsInfo.push(room.getInfo()); 107 | }; 108 | 109 | 110 | StorageInterface.prototype.popRoom = function(room){ 111 | var id = (room.id ? room.id : room); 112 | delete this.rooms[id]; 113 | for (var i = 0; i < this.roomsInfo.length; i++){ 114 | if (this.roomsInfo[i].room == id) { 115 | this.roomsInfo.splice(i, 1); 116 | return true; 117 | } 118 | } 119 | logger.err('StorageInterface.popRoom', "room not exists in roomsInfo id:", id, 1); 120 | }; 121 | 122 | 123 | StorageInterface.prototype.getRoom = function(id){ 124 | return this.rooms[id]; 125 | }; 126 | 127 | 128 | StorageInterface.prototype.getRooms = function(){ 129 | return this.roomsInfo; 130 | }; 131 | 132 | 133 | //____________ Chat ___________ 134 | StorageInterface.prototype.pushMessage = function(message){ 135 | this.messages.unshift(message); 136 | if (message.target == this.server.game) this.lastMessages.unshift(message); 137 | if (this.lastMessages.length>this.LAST_MSG_COUNT) this.lastMessages.pop(); 138 | }; 139 | 140 | StorageInterface.prototype.getMessages = function(count, time, target, sender){ 141 | var self = this; 142 | return new Promise(function(res){ 143 | if (sender) { 144 | res([]); 145 | return; 146 | } 147 | if (!time) { // public messages 148 | res(self.lastMessages); 149 | return; 150 | } 151 | for (var i = 0; i < self.messages.length; i++){ 152 | if (self.messages[i].time < time){ 153 | res(self.messages.slice(i, i+count)); 154 | return; 155 | } 156 | } 157 | res([]); 158 | }); 159 | }; 160 | 161 | StorageInterface.prototype.banUser = function(userId, timeEnd, reason){ 162 | this.bans.push({ 163 | userId:userId, 164 | timeEnd:timeEnd, 165 | timeStart: Date.now(), 166 | reason: reason 167 | }); 168 | }; 169 | 170 | 171 | 172 | StorageInterface.prototype.deleteMessage = function(id){ 173 | 174 | }; 175 | 176 | 177 | StorageInterface.prototype.checkIsBanned = function (userId) { 178 | for (var i = 0; i < this.bans.length; i++) { 179 | logger.log('StorageInterface.checkIsBanned, user is banned', userId, 3); 180 | if (this.bans[i].userId == userId && this.bans[i].timeEnd > Date.now()) { 181 | return this.bans[i]; 182 | } 183 | } 184 | 185 | return false; 186 | }; 187 | 188 | 189 | //____________ History ___________ 190 | StorageInterface.prototype.pushGame = function(save){ 191 | this.games.push(save); 192 | var game = { 193 | timeStart: save.timeStart, 194 | timeEnd: save.timeEnd, 195 | players: save.players, 196 | mode: save.mode, 197 | winner: save.winner, 198 | action: save.action, 199 | userData: save.userData 200 | }; 201 | this.history.push(game); 202 | }; 203 | 204 | 205 | StorageInterface.prototype.getGame = function(user, id){ 206 | return new Promise(function(res) { 207 | for (var i = 0; i < this.games.length; i++) { 208 | if (this.games[i].timeEnd == id) { 209 | res(this.games[i]); 210 | return; 211 | } 212 | } 213 | }.bind(this)); 214 | }; 215 | 216 | 217 | StorageInterface.prototype.getHistory = function(userId, mode){ 218 | return Promise.all([ 219 | new Promise(function(res) { // get history 220 | var history = []; 221 | for (var i = this.history.length - 1; i >= 0; i--) { 222 | if (this.history[i].mode == mode && this.history[i].players.indexOf(userId) > -1){ 223 | this.history[i]._id = this.history[i].timeEnd; 224 | history.push(this.history[i]); 225 | } 226 | } 227 | res(history); 228 | }.bind(this)), 229 | new Promise(function(res) { // get penalties 230 | res(null); 231 | }.bind(this)) 232 | ]); 233 | }; 234 | 235 | 236 | //_____________ Ratings ____________ 237 | StorageInterface.prototype.updateRatings = function(mode) { 238 | var users = [], i, user, self = this; 239 | return new Promise(function (res, rej) { 240 | for (i = 0; i < self.allUsers.length; i++) 241 | if (self.allUsers[i][mode]['games'] > 0) 242 | users.push(self.allUsers[i]); 243 | users.sort(function (a, b) { 244 | return b[mode]['ratingElo'] - a[mode]['ratingElo']; 245 | }); 246 | for (i = 0; i < users.length; i++) { 247 | users[i][mode]['rank'] = i + 1; 248 | user = self.getUser(users[i].userId); 249 | if (user) { 250 | logger.log('StorageInterface.updateRatings', user[mode]['rank'], i + 1 , 3); 251 | user[mode]['rank'] = i + 1; 252 | } 253 | } 254 | res(users); 255 | }); 256 | 257 | }; 258 | 259 | 260 | StorageInterface.prototype.getRatings = function(userId, params){ 261 | return new Promise(function (res, rej) { 262 | var allUsers = [], infoUser, mode = params.mode; 263 | for (var i = 0; i < this.allUsers.length; i++) { 264 | if (this.allUsers[i][mode]['rank'] > 0) 265 | allUsers.push(this.allUsers[i]); 266 | if (this.allUsers[i].userId == userId) infoUser = this.allUsers[i]; 267 | } 268 | allUsers.sort(function (a, b) { 269 | return b[mode]['ratingElo'] - a[mode]['ratingElo']; 270 | }); 271 | res(allUsers); 272 | }.bind(this)); 273 | }; -------------------------------------------------------------------------------- /lib/engine.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | /** 3 | * init logged user mode data, set your default scores here 4 | * @param mode 5 | * @param modeData 6 | * @returns {*} 7 | */ 8 | initUserData: function(mode, modeData){ 9 | return modeData; 10 | }, 11 | /** 12 | * on round begin init something 13 | * @param room 14 | * @returns {{inviteData: (*|userData.inviteData|Room.inviteData)}} 15 | */ 16 | initGame: function (room) { 17 | return { 18 | inviteData: room.inviteData 19 | } 20 | }, 21 | 22 | /** 23 | * on round begin set's first player 24 | * @param room 25 | * @returns {{player: Object}} 26 | */ 27 | setFirst: function (room) { 28 | if (!room.game.first) return room.owner; 29 | return room.getOpponent(room.game.first); 30 | }, 31 | 32 | /** 33 | * every turn do something and send this to all 34 | * @param room 35 | * @param user 36 | * @param turn 37 | * @param type {'turn'|'timeout'} 38 | * @returns {turn} 39 | */ 40 | doTurn: function(room, user, turn, type){ 41 | if (type == 'timeout'){ 42 | // this is user timeout 43 | } 44 | return turn; 45 | }, 46 | 47 | /** 48 | * every user turn checks switch player to next 49 | * @param room 50 | * @param user 51 | * @param turn 52 | * @param type {'turn'|'timeout'} 53 | * @returns {*} 54 | */ 55 | switchPlayer: function(room, user, turn, type){ 56 | if (type == 'timeout'){ 57 | // this is user timeout 58 | } 59 | return room.getOpponent(user); 60 | }, 61 | 62 | /** 63 | * every user event. Do what you need and send event in room or to user 64 | * @param room 65 | * @param user 66 | * @param event 67 | * @returns {{event: *, target: null|Room|User, user: null|userId} || Array} 68 | */ 69 | userEvent: function(room, user, event){ 70 | return { 71 | event: event, 72 | target: room, 73 | user: user.userId 74 | } 75 | }, 76 | 77 | /** 78 | * every user turn and on round start. Do what you need and send event in room or to user 79 | * @param room 80 | * @param user 81 | * @param turn 82 | * @param roundStart flag, event on round start 83 | * @returns {{event: *, target: null|Room|User, user: null|userId} || Array} 84 | */ 85 | gameEvent: function(room, user, turn, roundStart){ 86 | return null; 87 | }, 88 | 89 | /** 90 | * every user turn checks game result 91 | * @param room 92 | * @param user 93 | * @param turn 94 | * @param type {'turn'|'event'|'timeout'} 95 | * @returns {*} false - game not end, null - draw, {winner : user} - winner 96 | */ 97 | getGameResult: function(room, user, turn, type){ 98 | switch (type){ 99 | case 'timeout': 100 | if (type == 'timeout'){ 101 | // if user have max timeouts, other win 102 | if (room.data[user.userId].timeouts == room.maxTimeouts){ 103 | return { 104 | winner: room.getOpponent(user), 105 | action: 'timeout' 106 | }; 107 | } else return false; 108 | } 109 | break; 110 | case 'event': 111 | if (turn.type){ 112 | return false; 113 | } 114 | break; 115 | case 'turn': 116 | switch (turn.result){ 117 | case 0: // win other player 118 | return { 119 | winner: room.getOpponent(user) 120 | }; 121 | break; 122 | case 1: // win current player 123 | return { 124 | winner: user 125 | }; 126 | break; 127 | case 2: // draw 128 | return { 129 | winner: null 130 | }; 131 | break; 132 | default: // game isn't end 133 | return false; 134 | } 135 | break; 136 | } 137 | }, 138 | 139 | /** 140 | * set players score player[room.mode]['your_score'], winner: result: winner 141 | * returns what you to send 142 | * @param room 143 | * @param result 144 | */ 145 | getUsersScores: function(room, result){ 146 | return result; 147 | }, 148 | 149 | /** 150 | * check user sign is right 151 | * @param user 152 | * @returns {data.userId|*|.data.userId|ChatManager.message.userId|userId|query.$or.userId} 153 | */ 154 | checkSign: function(user){ 155 | return (user.userId && user.userName && user.sign); 156 | }, 157 | 158 | /** 159 | * do something on admin message 160 | * @param admin 161 | * @param type 162 | * @param data 163 | */ 164 | adminAction: function(admin, type, data){ 165 | 166 | } 167 | }; -------------------------------------------------------------------------------- /lib/game_manager.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var Room = require('./room.js'); 3 | var util = require('util'); 4 | var logger = require('./logger.js'); 5 | 6 | module.exports = GameManager; 7 | 8 | function GameManager(server){ 9 | EventEmitter.call(this); 10 | 11 | this.server = server; 12 | this.engine = server.engine; 13 | this.defaultEngine = server.defaultEngine; 14 | this.turnTime = server.conf.turnTime * 1000; 15 | this.maxTimeouts = server.conf.maxTimeouts; 16 | this.maxOfflineTimeouts = server.conf.maxOfflineTimeouts; 17 | this.timeMode = server.conf.timeMode; 18 | this.timeStartMode = server.conf.timeStartMode; 19 | this.addTime = server.conf.addTime; 20 | this.corTime = server.conf.corTime; 21 | this.clearTimeouts = server.conf.clearTimeouts; 22 | this.minTurns = server.conf.minTurns; 23 | this.enableGames = true; 24 | 25 | // bindEvents 26 | server.on('user_leave', this.onUserDisconnect.bind(this)); 27 | server.on('user_relogin', this.onUserRelogin.bind(this)); 28 | server.inviteManager.on('invite_accepted', this.onInviteAccepted.bind(this)); 29 | } 30 | 31 | util.inherits(GameManager, EventEmitter); 32 | 33 | 34 | GameManager.prototype.onMessage = function(message, type){ 35 | var room; 36 | if (type == 'spectate') { 37 | room = this.server.storage.getRoom(message.data.roomId); 38 | } else room = this.getUserRoom(message.sender, type != 'leave'); 39 | 40 | if (!room){ 41 | logger.err('GameManager', 'no room to continue', message, 2); 42 | this.sendError(message.sender, 'no room!'); 43 | return; 44 | } 45 | 46 | switch (type){ 47 | case 'ready': // player ready to play 48 | this.setUserReady(room, message.sender, message.data); 49 | break; 50 | case 'turn': // all players turns 51 | this.onUserTurn(room, message.sender, message.data); 52 | break; 53 | case 'event': // all events, draw, throw, turn back, others 54 | this.onUserEvent(room, message.sender, message.data); 55 | break; 56 | case 'spectate': // user begin spectate 57 | this.onUserSpectate(room, message.sender); 58 | break; 59 | case 'leave': // user leave room 60 | if (room.players.indexOf(message.sender) != -1) 61 | this.onUserLeave(room, message.sender); 62 | else 63 | if (room.spectators.indexOf(message.sender) != -1) this.onSpectatorLeave(room, message.sender); 64 | else logger.err('GameManager.onMessage leave', 'user not a player and not a spectator in room', room.id, 1); 65 | break; 66 | } 67 | }; 68 | 69 | 70 | GameManager.prototype.onUserDisconnect = function(user){ 71 | var room = this.getUserRoom(user, true); // get room where user is player 72 | if (room && (!room.isPlaying() || this.server.conf.loseOnLeave)) { 73 | this.onUserLeave(user.currentRoom, user); 74 | } 75 | if (!room){ 76 | room = this.getSpectatorRoom(user); // get room where user is spectator 77 | if (room){ 78 | this.onSpectatorLeave(room, user); 79 | } 80 | } 81 | }; 82 | 83 | 84 | GameManager.prototype.onUserRelogin = function(user){ 85 | var room = user.currentRoom; 86 | if (room) { 87 | user.socket.enterRoom(room.id); // new socket enter game room 88 | this.server.router.send({ 89 | module: 'game_manager', 90 | type: 'game_restart', 91 | target: user, 92 | data: room.getGameData() 93 | }); 94 | room.game.askDraw = null; 95 | if (!this.server.conf.reconnectOldGame || !room.isPlaying()) 96 | this.onUserLeave(room, user); 97 | } 98 | }; 99 | 100 | 101 | GameManager.prototype.onInviteAccepted = function(invite){ 102 | logger.log('GameManager.onInviteAccepted', 'invite_accepted',invite.owner.userId, 3); 103 | if (!invite.owner || !invite.players || invite.players.length<2 || !invite.data){ 104 | logger.err('GameManager.onInviteAccepted', 'wrong invite!', invite, 1); 105 | return; 106 | } 107 | if (!this.enableGames){ 108 | logger.warn('GameManager.onInviteAccepted', 'new games disabled ', 1); 109 | return; 110 | } 111 | 112 | var ip1 = invite.players[0].socket.ip; 113 | var ip2 = invite.players[1].socket.ip; 114 | var admin = invite.players[0].isAdmin || invite.players[1].isAdmin; 115 | logger.log('GameManager.onInviteAccepted', 'ips', ip1, ip2, 3); 116 | if (!this.server.conf.enableIpGames && ip1 == ip2 && ip1 != null) { 117 | if (!admin && ip1.indexOf('127.0.0.1') != -1 && ip1.indexOf(':192.168.') != -1){ 118 | logger.warn('GameManager.onInviteAccepted', 'play from one ip ', invite.owner.userId, ip1, 1); 119 | return; 120 | } 121 | } 122 | 123 | // leave spectators room 124 | var player; 125 | for (var i = 0; i < invite.players.length; i++){ 126 | player = invite.players[i]; 127 | if (this.getSpectatorRoom(player)){ 128 | this.onSpectatorLeave(player.currentRoom, player); 129 | } 130 | if (this.getUserRoom(player)){ 131 | logger.err('GameManager.onInviteAccepted', 'player already in room ', player.userId, 1); 132 | return; 133 | } 134 | } 135 | 136 | this.createGame(invite.owner, invite.players, invite.data); 137 | }; 138 | 139 | 140 | GameManager.prototype.createGame = function(owner, players, data){ 141 | delete data.from; 142 | delete data.target; 143 | if (!data.mode) { 144 | logger.err('GameManager.createGame', 'game mode undefined! wrong invite!', data, owner.userId); 145 | return; 146 | } 147 | var room = this.createRoom(owner, players, data); 148 | var info = room.getInfo(); 149 | 150 | this.server.router.send({ 151 | module: 'server', 152 | type: 'new_game', 153 | target: this.server.game, 154 | data: info 155 | }); 156 | 157 | this.server.storage.pushRoom(room); 158 | }; 159 | 160 | 161 | GameManager.prototype.createRoom = function(owner, players, data){ 162 | var id = this.generateRoomId(owner, data.mode); 163 | var room = new Room(id, owner, players, data); 164 | room.saveHistory = data.saveHistory !== false; 165 | room.saveRating = data.saveRating !== false; 166 | room.turnTime = data.turnTime*1000 || this.turnTime; 167 | room.timeMode = data.timeMode || this.timeMode; 168 | room.timeStartMode = data.timeStartMode || this.timeStartMode; 169 | room.addTime = data.addTime || this.addTime; 170 | if (!room.turnTime || room.turnTime<0) room.turnTime = this.turnTime; 171 | room.takeBacks = +data.takeBacks || this.server.conf.takeBacks; 172 | room.maxTimeouts = this.maxTimeouts; 173 | room.minTurns = this.minTurns; 174 | for (var i = 0; i < players.length; i++) { 175 | players[i].enterRoom(room); 176 | } 177 | 178 | return room; 179 | }; 180 | 181 | 182 | GameManager.prototype.setUserReady = function(room, user, ready){ 183 | logger.log('GameManager.setUserReady room:', room.id, 'user:', user.userId, ready, 3); 184 | if (typeof ready != "boolean") ready = true; 185 | if (room.game.state != "waiting") { 186 | logger.err('GameManager.setUserReady', 'game already started!', room, user.userId, ready, 1); 187 | return; 188 | } 189 | room.data[user.userId].ready = ready; 190 | 191 | this.server.router.send({ 192 | module: 'game_manager', 193 | type: 'ready', 194 | target: room, 195 | data: { 196 | user:user.userId, 197 | ready:ready 198 | } 199 | }); 200 | 201 | if (room.checkPlayersReady()){ // all users ready 202 | // initializing game before start, and get data to send players 203 | var game = room.game; 204 | game.initData = this.initGame(room); 205 | game.state = "playing"; 206 | game.history = []; 207 | game.shistory = ""; 208 | 209 | this.server.router.send({ 210 | module: 'game_manager', 211 | type: 'round_start', 212 | target: room, 213 | data: game.initData 214 | }); 215 | 216 | if (typeof this.engine.gameEvent == "function") this.sendEvent(room, this.engine.gameEvent(room, null, null, true)); 217 | 218 | if (room.timeStartMode == 'after_round_start'){ 219 | this.updateTime(room, game.current); 220 | } 221 | } 222 | }; 223 | 224 | 225 | GameManager.prototype.initGame = function(room) { 226 | var userData = { 227 | inviteData: room.inviteData 228 | }; 229 | // TODO: async initGame and error handler 230 | if (typeof this.engine.initGame == "function") 231 | userData = this.engine.initGame(room) || userData; 232 | if (typeof this.engine.setFirst == "function") 233 | room.game.first = this.engine.setFirst(room); 234 | else 235 | room.game.first = this.defaultEngine.setFirst(room); 236 | if (!room.game.first || !room.game.first.userId){ 237 | throw new Error('first player is undefined! '+ room.id) 238 | } 239 | room.game.current = room.game.first; 240 | room.game.askDraw = null; 241 | room.game.askTakeBack = null; 242 | room.game.turns = 0; 243 | room.game.timeouts = 0; 244 | room.game.timeStart = Date.now(); 245 | room.game.turnStartTime = null; 246 | room.userTurnTime = null; 247 | room.corTime = this.corTime; 248 | 249 | userData.first = room.game.current.userId; 250 | userData.id = room.id; 251 | userData.owner = room.owner.userId; 252 | userData.players = []; 253 | userData.score = room.getScore(); 254 | userData.turnTime = room.turnTime; 255 | userData.timeMode = room.timeMode; 256 | userData.timeStartMode = room.timeStartMode; 257 | userData.addTime = room.addTime; 258 | userData.saveHistory = room.saveHistory; 259 | userData.saveRating = room.saveRating; 260 | for (var i = 0; i < room.players.length; i++) { 261 | userData.players.push(room.players[i].userId); 262 | room.data[room.players[i].userId].userTurnTime = room.turnTime; 263 | room.data[room.players[i].userId].userTotalTime = 0; 264 | room.data[room.players[i].userId].focusChanged = false; 265 | room.data[room.players[i].userId].userTurns = 0; 266 | room.data[room.players[i].userId].userUnfocusedTurns = 0; 267 | } 268 | return userData; 269 | }; 270 | 271 | 272 | GameManager.prototype.onUserTurn = function(room, user, turn){ 273 | logger.log('GameManager.onUserTurn room:', room.id, 'user:', user.userId, 3); 274 | var game = room.game, 275 | userTurn, 276 | isGameEnd, 277 | nextPlayer; 278 | if (game.state != 'playing'){ 279 | this.sendError(user, 'game_not_started!'); 280 | return; 281 | } 282 | // check user is current, check turn is valid, ask engine what to do, send to all in room 283 | if (game.current != user) { // wrong user 284 | this.sendError(user, 'not_your_turn'); 285 | return; 286 | } 287 | 288 | if (turn.action == 'timeout' || turn.type || turn.nextPlayer || turn.userTurnTime){ 289 | logger.warn('GameManager.onUserTurn, usage some reserved properties in turn: ', turn, user.userId, 1); 290 | } 291 | 292 | // remove server properties 293 | if (turn.action == 'timeout') delete turn.action; 294 | if (turn.userTurnTime) delete turn.userTurnTime; 295 | if (turn.nextPlayer) delete turn.nextPlayer; 296 | if (turn.type) delete turn.type; 297 | 298 | // do turn in engine 299 | if (typeof this.engine.doTurn == "function") userTurn = this.engine.doTurn(room, user, turn, 'turn'); 300 | else userTurn = turn; 301 | if (!userTurn || typeof userTurn != "object") { // wrong turn 302 | logger.err('GameManager.onUserTurn, wrong turn: ', userTurn, turn, 1); 303 | this.server.router.send({ 304 | module: 'game_manager', 305 | type: 'error', 306 | target: user, 307 | data: 'wrong_turn' 308 | }); 309 | return; 310 | } 311 | 312 | // switch player 313 | nextPlayer = this.switchPlayer(room, user, userTurn, 'turn'); 314 | if (nextPlayer != game.current ) { // if switch player 315 | if (room.timeout) clearTimeout(room.timeout); 316 | if (this.clearTimeouts) { 317 | room.data[game.current.userId].timeouts = 0; 318 | } 319 | userTurn.nextPlayer = nextPlayer.userId; 320 | room.game.turns++; 321 | room.askTakeBack = null; 322 | room.data[user.userId].userTurns++; 323 | if (room.data[user.userId].focusChanged){ 324 | logger.err('GameManager.onUserTurn, turn after focus change ', user.userId, 3); 325 | room.data[user.userId].userUnfocusedTurns++; 326 | room.data[user.userId].focusChanged = false; 327 | } 328 | } 329 | room.savePlayerTurn(userTurn, nextPlayer); 330 | 331 | // send turn 332 | this.server.router.send({ 333 | module: 'game_manager', 334 | type: 'turn', 335 | target: room, 336 | data: {user:user.userId, turn:userTurn} 337 | }); 338 | 339 | // send game event on user turn if need 340 | if (!isGameEnd && typeof this.engine.gameEvent == "function") 341 | this.sendEvent(room, this.engine.gameEvent(room, user, turn, false)); 342 | // check endGame 343 | isGameEnd = this.checkGameEnd(room, user, turn, 'turn'); 344 | 345 | // set current player and reset timeout 346 | if (!isGameEnd) { 347 | this.updateTime(room, nextPlayer); 348 | } 349 | }; 350 | 351 | 352 | GameManager.prototype.updateTime = function(room, nextPlayer){ 353 | var game = room.game; 354 | var userTurnTime = game.turnStartTime ? Date.now() - game.turnStartTime : 0; 355 | room.data[game.current.userId].userTotalTime += userTurnTime; 356 | if (nextPlayer != game.current && room.timeMode == 'dont_reset') { 357 | logger.log('GameManager.updateTime time:', userTurnTime, room.data[game.current.userId].userTotalTime, room.data[game.current.userId].userTurnTime, 3); 358 | room.data[game.current.userId].userTurnTime -= userTurnTime; 359 | // TODO: check if user time is out, time can be < 0 360 | } 361 | if (nextPlayer != game.current || 362 | room.timeMode == 'reset_every_turn' || 363 | (room.timeStartMode == 'after_turn' && !room.timeout) || 364 | (room.timeStartMode == 'after_round_start' && !room.timeout) || 365 | (room.timeMode == 'common' && !room.timeout) 366 | ){ 367 | clearTimeout(room.timeout); 368 | if (nextPlayer != game.current){ 369 | room.data[game.current.userId].userTurnTime += room.addTime; 370 | } 371 | room.game.current = nextPlayer; 372 | room.game.turnStartTime = Date.now(); 373 | 374 | var turnTime = room.getTurnTime(); 375 | if (turnTime > 1000 && (room.timeMode == 'reset_every_turn' || room.timeMode == 'reset_every_switch')) { 376 | turnTime += this.corTime; 377 | } 378 | 379 | logger.log('GameManager.updateTime timeout:', turnTime, this.corTime, room.getTurnTime(), 3); 380 | 381 | room.timeout = setTimeout(function () { 382 | this.onTimeout(room, room.game.current); 383 | }.bind(this),turnTime) 384 | } 385 | }; 386 | 387 | 388 | GameManager.prototype.switchPlayer = function(room, user, turn, type){ 389 | var nextPlayer; 390 | if (typeof this.engine.switchPlayer == "function") nextPlayer = this.engine.switchPlayer(room, user, turn, type); 391 | if (!nextPlayer) nextPlayer = this.defaultEngine.switchPlayer(room, user, turn, type); 392 | return nextPlayer; 393 | }; 394 | 395 | 396 | GameManager.prototype.checkGameEnd = function(room, user, turn, type){ 397 | var result = false; // false - game not end, null - draw, user - winner 398 | if (typeof this.engine.getGameResult == "function") 399 | result = this.engine.getGameResult(room, user, turn, type); 400 | else 401 | result = this.defaultEngine.getGameResult(room, user, turn, type); 402 | if (result) { // game end 403 | this.onRoundEnd(room, result); 404 | return true; 405 | } 406 | return false; 407 | }; 408 | 409 | 410 | GameManager.prototype.onUserEvent = function(room, user, event){ 411 | // check event type, throw, ask draw, ask moveback 412 | if (room.game.state != "playing") { 413 | logger.err('event in not started game room:', room.id, user.userId, 2); 414 | this.sendError(user, 'event in not started game room: ' + room.id); 415 | return; 416 | } 417 | if (!event.type) { 418 | logger.err('wrong event type ', room.id, user.userId, 1); 419 | this.sendError(user, 'wrong event type room: ' + room.id); 420 | return; 421 | } 422 | switch (event.type){ 423 | case 'throw': this.onThrow(room, user, event.type); break; 424 | case 'draw': this.onDraw(room, user, event); break; 425 | case 'back': this.onTakeBack(room, user, event); break; 426 | case 'focus': this.onWindowFocus(room, user, event); break; 427 | default: 428 | logger.log('GameManager.onUserEvent', event.type, user.userId, 2); 429 | if (typeof this.engine.userEvent == "function"){ 430 | this.sendEvent(room, this.engine.userEvent(room, user, event)); 431 | } 432 | var isGameEnd = this.checkGameEnd(room, user, event, 'event'); 433 | } 434 | }; 435 | 436 | 437 | GameManager.prototype.sendEvent = function(room, data) { 438 | if (!data) return; 439 | 440 | if (data.length > 0) { // array of users and their events 441 | for (var i = 0; i < data.length; i++) { 442 | this.sendEvent(room, data[i], true); 443 | } 444 | return; 445 | } 446 | 447 | if (data.target) { 448 | if (!data.event || !data.event.type) { 449 | logger.warn('GameManager.sendEvent', 'wrong event', data, 1); 450 | return; 451 | } 452 | 453 | room.savePlayerEvent(data.target, data.event); 454 | 455 | this.server.router.send({ 456 | module: 'game_manager', 457 | type: 'event', 458 | target: data.target, 459 | data: data.event 460 | }); 461 | } 462 | }; 463 | 464 | 465 | GameManager.prototype.onThrow = function(room, user, event){ 466 | event = event || 'throw'; 467 | for (var i = 0; i < room.players.length; i++) 468 | if (room.players[i] != user) { 469 | this.onRoundEnd(room, { 470 | winner: room.players[i], 471 | action: event 472 | }); 473 | return; 474 | } 475 | }; 476 | 477 | 478 | GameManager.prototype.onDraw = function(room, user, event){ 479 | // TODO: check user can ask draw 480 | switch (event.action){ 481 | case 'ask': 482 | if (!room.game.askDraw) { 483 | room.game.askDraw = user; 484 | this.server.router.send({ 485 | module: 'game_manager', 486 | type: 'event', 487 | target: room, 488 | data: { 489 | user: user.userId, 490 | type: 'draw', 491 | action: 'ask' 492 | } 493 | }); 494 | } else { 495 | if (room.game.askDraw == user) { // already asked 496 | logger.log('GameManager.onDraw', 'user already ask draw', user.userId, 2); 497 | } else { // draw 498 | logger.log('GameManager.onDraw', 'auto draw', user.userId, room.game.askDraw.userId, 2); 499 | this.onRoundEnd(room, {action: 'draw'}); 500 | } 501 | } 502 | break; 503 | case 'cancel': 504 | if (room.game.askDraw && room.game.askDraw != user) { 505 | this.server.router.send({ 506 | module: 'game_manager', 507 | type: 'event', 508 | target: room.game.askDraw, 509 | data: { 510 | user: user.userId, 511 | type: 'draw', 512 | action: 'cancel' 513 | } 514 | }); 515 | room.game.askDraw = null; 516 | } else { 517 | logger.warn('GameManager.onDraw', 'wrong cancel draw', user.userId, room.game.askDraw, 2); 518 | } 519 | break; 520 | case 'accept': 521 | if (room.game.askDraw && room.game.askDraw != user) { 522 | logger.log('GameManager.onDraw', 'draw', user.userId, room.game.askDraw.userId, 2); 523 | this.onRoundEnd(room, {action: 'draw'}); 524 | room.game.askDraw = null; 525 | } else { 526 | logger.warn('GameManager.onDraw', 'wrong accept draw', user.userId, room.game.askDraw, 2); 527 | } 528 | break; 529 | } 530 | 531 | }; 532 | 533 | 534 | GameManager.prototype.onTakeBack = function(room, user, event){ 535 | switch (event.action) { 536 | case 'take': 537 | if (room.askTakeBack){ // send cancel to other user 538 | if (room.askTakeBack == user) { 539 | return; 540 | } else { 541 | this.server.router.send({ 542 | module: 'game_manager', 543 | type: 'event', 544 | target: room.askTakeBack, 545 | data: {type: 'back', action: 'cancel', user: user.userId} 546 | }); 547 | room.askTakeBack = null; 548 | } 549 | } 550 | if (room.data[user.userId].takeBacks < room.takeBacks){ // doTakeBack 551 | this.doTakeBack(room, user); 552 | } else { // ask 553 | room.askTakeBack = user; 554 | this.server.router.send({ 555 | module: 'game_manager', 556 | type: 'event', 557 | target: room.id, 558 | data: {type: 'back', action: 'ask', user: user.userId} 559 | }); 560 | } 561 | break; 562 | case 'accept': 563 | if (room.askTakeBack && room.askTakeBack != user) 564 | this.doTakeBack(room, room.askTakeBack); 565 | break; 566 | case 'cancel': 567 | if (room.askTakeBack && room.askTakeBack != user) 568 | this.server.router.send({ 569 | module: 'game_manager', 570 | type: 'event', 571 | target: room.askTakeBack, 572 | data: {type: 'back', action: 'cancel', user: user.userId} 573 | }); 574 | this.askTakeBack = null; 575 | break; 576 | } 577 | }; 578 | 579 | 580 | GameManager.prototype.doTakeBack = function(room, user) { 581 | var game = room.game; 582 | room.askTakeBack = null; 583 | if (game.history.length > 0 && game.turns > 0) { 584 | for (var i = game.history.length - 2; i >= 0; i--) { // find previous user turn 585 | var turn = game.history[i]; 586 | if (turn.length > 0) turn = turn[turn.length - 1]; 587 | if (turn.nextPlayer == user.userId) { 588 | break; 589 | } 590 | if (i == 0) i = -1; 591 | } 592 | var count = game.history.length - i - 1; 593 | if (i < 0) { // user was first, cut all turns 594 | if (game.first == user) { 595 | count = game.turns; 596 | } else { 597 | count = 0; 598 | } 599 | i = game.history.length - count - 1; 600 | } 601 | logger.log('GameManager.onTakeBack; cut count:', count, ' total turns: ', game.turns, 'history length:', game.history.length, i, 3); 602 | logger.log('GameManager.onTakeBack; shistory', game.shistory, 3); 603 | if (count > 0) { // cut turns 604 | for (var j = 0; j < count; j++) { 605 | game.shistory = game.shistory.substring(0, game.shistory.lastIndexOf('@')) 606 | } 607 | logger.log('GameManager.onTakeBack; shistory', game.shistory, 3); 608 | game.history.splice(i + 1); 609 | game.turns -= count; 610 | 611 | this.updateTime(room, user); 612 | this.sendTakeBack(room, user); 613 | } 614 | } 615 | }; 616 | 617 | 618 | GameManager.prototype.sendTakeBack = function(room, user){ 619 | room.data[user.userId].takeBacks++; 620 | this.server.router.send({ 621 | module: 'game_manager', 622 | type: 'event', 623 | target: room.id, 624 | data: { 625 | user: user.userId, 626 | history: room.game.shistory, 627 | type: 'back', 628 | action: 'take' 629 | } 630 | }); 631 | }; 632 | 633 | 634 | GameManager.prototype.onWindowFocus = function(room, user, event){ 635 | switch (event.action){ 636 | case 'lost': 637 | this.server.router.send({ 638 | module: 'game_manager', 639 | type: 'event', 640 | target: room.id, 641 | data: { 642 | user: user.userId, 643 | type: 'focus', 644 | action: 'lost' 645 | } 646 | }); 647 | break; 648 | case 'has': 649 | room.data[user.userId].focusChanged = true; 650 | this.server.router.send({ 651 | module: 'game_manager', 652 | type: 'event', 653 | target: room.id, 654 | data: { 655 | user: user.userId, 656 | type: 'focus', 657 | action: 'has' 658 | } 659 | }); 660 | break; 661 | } 662 | }; 663 | 664 | 665 | GameManager.prototype.onTimeout = function(room, user){ 666 | room.data[user.userId].timeouts++; 667 | clearTimeout(room.timeout); 668 | logger.log('GameManager.onTimeout;', room.id, user.userId, room.data[user.userId].timeouts, 2); 669 | if (!room.hasOnlinePlayer()){ 670 | this.onThrow(room, user, 'timeout'); 671 | return; 672 | } 673 | // player auto skip turn, switch players 674 | var nextPlayer, turn = {action: 'timeout'}, game = room.game, isGameEnd; 675 | if (typeof this.engine.doTurn == 'function'){ 676 | turn = this.engine.doTurn(room, user, turn, 'timeout'); 677 | } else { 678 | turn = this.defaultEngine.doTurn(room, user, turn, 'timeout'); 679 | } 680 | 681 | if (turn) { 682 | nextPlayer = this.switchPlayer(room, user, turn, 'timeout'); 683 | //save end send timeout turn 684 | if (turn.action == 'timeout' || turn.type) { 685 | turn.user = user.userId; 686 | if (!turn.type) turn.type = turn.action; 687 | } else { 688 | if (room.userTurnTime) { 689 | turn.userTurnTime = room.userTurnTime 690 | } 691 | } 692 | game.timeouts++; 693 | if (nextPlayer != game.current) { // if switch player 694 | turn.nextPlayer = nextPlayer.userId; 695 | room.game.turns++; 696 | room.askTakeBack = null; 697 | } 698 | room.savePlayerTurn(turn, nextPlayer); 699 | if (turn.action == 'timeout' || turn.type) { // send event 700 | this.server.router.send({ 701 | module: 'game_manager', 702 | type: 'event', 703 | target: room, 704 | data: turn 705 | }); 706 | } else { //send turn 707 | this.server.router.send({ 708 | module: 'game_manager', 709 | type: 'turn', 710 | target: room, 711 | data: { user: user.userId, turn: turn } 712 | }); 713 | } 714 | } 715 | // check end game 716 | isGameEnd = this.checkGameEnd(room, user, turn, 'timeout'); 717 | // send game event on user turn if need 718 | if (!isGameEnd && typeof this.engine.gameEvent == "function") 719 | this.sendEvent(room, this.engine.gameEvent(room, user, turn, false)); 720 | 721 | // game no ended? but offline user have max timeouts 722 | if (!isGameEnd && !user.isConnected && room.data[user.userId].timeouts == this.maxOfflineTimeouts){ 723 | this.onThrow(room, user, 'timeout'); 724 | return; 725 | } 726 | // switch player, set timeout 727 | if (!isGameEnd) { 728 | this.updateTime(room, nextPlayer); 729 | } 730 | }; 731 | 732 | 733 | GameManager.prototype.onUserPause = function(room, user) { 734 | // если игрок текущий и игра идет, не пауза и не ожидание 735 | // ставим игру на паузу 736 | }; 737 | 738 | 739 | GameManager.prototype.onUserSpectate = function(room, user){ 740 | if (!this.server.conf.spectateEnable) return; 741 | if (this.getUserRoom(user, false)){ 742 | logger.err('GameManager.onUserSpectate', 'user already in room ', user.currentRoom.id, 2); 743 | return; 744 | } 745 | room.spectators.push(user); 746 | user.enterRoom(room); 747 | // send user room data 748 | this.server.router.send({ 749 | module: 'game_manager', 750 | type: 'spectate', 751 | target: user, 752 | data: room.getGameData() 753 | }); 754 | 755 | this.server.router.send({ 756 | module: 'game_manager', 757 | type: 'spectator_join', 758 | target: room, 759 | data: { 760 | user: user.userId, 761 | room: room.id 762 | } 763 | }); 764 | }; 765 | 766 | 767 | GameManager.prototype.onSpectatorLeave = function(room, user){ 768 | logger.log('GameManager.onSpectatorLeave', user.userId, room.id, 3); 769 | for (var i = 0; i < room.spectators.length; i++){ 770 | if (room.spectators[i].userId == user.userId) { 771 | room.spectators.splice(i, 1); 772 | try { // users can leave room and room will be closed before round spectate stop 773 | this.server.router.send({ 774 | module: 'game_manager', 775 | type: 'spectator_leave', 776 | target: room, 777 | data: { 778 | user: user.userId, 779 | room: room.id 780 | } 781 | }); 782 | } catch (e) { 783 | logger.err('GameManager.onSpectatorLeave, err:', e, 1); 784 | } 785 | user.leaveRoom(); 786 | return; 787 | } 788 | } 789 | }; 790 | 791 | 792 | GameManager.prototype.onUserLeave = function(room, user){ 793 | logger.log('GameManager.onUserLeave', user.userId, room.id, 2); 794 | var i, result; 795 | // other user win if game start 796 | if (room.game.state == "playing") 797 | for (i = 0; i < room.players.length; i++) 798 | if (room.players[i] != user) { 799 | result = { 800 | winner: room.players[i], 801 | action: 'user_leave' 802 | }; 803 | break; 804 | } 805 | // TODO: warn! async closing room 806 | this.onRoundEnd(room, result, function(){ 807 | if (room.hasOnlinePlayer() || room.spectators.length > 0) // check room isn't empty 808 | try { // users can leave room and room will be closed before round result send 809 | this.server.router.send({ 810 | module: 'game_manager', 811 | type: 'user_leave', 812 | target: room, 813 | data: user.userId 814 | }); 815 | } catch (e) { 816 | logger.err('GameManager.onUserLeave, err:', e, 1); 817 | } 818 | else { 819 | logger.warn('GameManager.onUserLeave, room:', room.id, 'no players online', 1); 820 | } 821 | 822 | logger.log('closeRoom', room.id, 3); 823 | for (i = 0; i < room.players.length; i++) room.players[i].leaveRoom(); 824 | for (i = 0; i < room.spectators.length; i++) room.spectators[i].leaveRoom(); 825 | this.server.storage.popRoom(room); 826 | 827 | this.server.router.send({ 828 | module: 'server', 829 | type: 'end_game', 830 | target: this.server.game, 831 | data: {players:room.getPlayersId(), room:room.id} 832 | }); 833 | }.bind(this)); 834 | }; 835 | 836 | 837 | GameManager.prototype.onRoundEnd = function(room, result, callback){ 838 | if (room.game.state != "playing") { 839 | logger.log('GameManager.onRoundEnd, room:', room.id, 'not playing! on user leave', 2); 840 | if (callback) callback(); 841 | return; 842 | } 843 | 844 | var self = this; 845 | if (room.timeout) clearTimeout(room.timeout); 846 | room.timeout = null; 847 | 848 | result.timeStart = room.game.timeStart; 849 | result.timeEnd = Date.now(); 850 | result.time = result.timeEnd - result.timeStart; 851 | 852 | if (typeof this.engine.getUsersScores == "function") result = this.engine.getUsersScores(room, result); 853 | if (result.winner && result.winner.userId) { 854 | result.winner = result.winner.userId; 855 | } 856 | result.save = result.save != null ? result.save : room.game.turns - room.game.timeouts >= room.minTurns; 857 | result.action = result.action != null ? result.action : 'game_over'; 858 | 859 | logger.log('GameManager.onRoundEnd, room:', room.id, 'winner:', result.winner, 860 | 'action:', result.action, 'save: ', result.save, 861 | 'saveHistory:', room.saveHistory, 'saveRating:', room.saveRating, room.turnTime!=this.turnTime?'turnTime: '+room.turnTime :'', 3); 862 | 863 | room.game.state = "waiting"; 864 | 865 | if (result.save) { 866 | room.games++; 867 | logger.log('GameManager.onRoundEnd, result:', room.data[room.players[0].userId].userTotalTime, room.data[room.players[1].userId].userTotalTime, 3); 868 | if (result.action == 'game_over'){ 869 | this.checkCheaters(room, result); 870 | } 871 | if (result.winner) room.data[result.winner].win++; 872 | this.server.ratingManager.computeNewRatings(room, result, function(){ 873 | logger.log('GameManager ratings computed', 3); 874 | self.sendGameResult(room, result, callback); 875 | }); 876 | } else { 877 | this.sendGameResult(room, result, callback); 878 | } 879 | }; 880 | 881 | 882 | GameManager.prototype.checkCheaters = function(room, result) { 883 | if (!this.server.conf.minUnfocusedTurns || !result || !result.winner) return; 884 | var user, turns, unfocusedTurns; 885 | for (var i = 0; i < room.players.length; i++){ 886 | if (room.players[i].userId == result.winner) { 887 | user = room.players[i]; 888 | turns = room.data[user.userId].userTurns; 889 | unfocusedTurns = room.data[user.userId].userUnfocusedTurns; 890 | logger.log('GameManager.checkCheaters, cheaters:', user.userId, turns, unfocusedTurns, 3); 891 | if (unfocusedTurns >= this.server.conf.minUnfocusedTurns 892 | && unfocusedTurns > (turns - 1) * this.server.conf.minPerUnfocusedTurns) { 893 | logger.log('GameManager.checkCheaters, cheater:', user.userId, user.userName, room.game.turns, turns, unfocusedTurns, 1); 894 | user[room.mode].timeLastCheatGame = result.timeEnd; 895 | user[room.mode].cheatWins = user[room.mode].cheatWins ? user[room.mode].cheatWins + 1 : 1; 896 | this.server.sendUserInfo(user); 897 | } 898 | return; 899 | } 900 | } 901 | }; 902 | 903 | 904 | GameManager.prototype.sendGameResult = function(room, result, callback){ 905 | logger.log('GameManager.sendGameResult, room:', room.id, result.action, 3); 906 | result.score = room.getScore(); 907 | result.ratings = {}; 908 | result.saveHistory = room.saveHistory; 909 | result.saveRating = room.saveRating; 910 | 911 | var user, i; 912 | for (i = 0; i < room.players.length; i++){ 913 | user = room.players[i]; 914 | room.data[user.userId].ready = false; 915 | room.data[user.userId].timeouts = 0; 916 | room.data[user.userId].takeBacks = 0; 917 | result.ratings[user.userId] = user[room.mode]; 918 | } 919 | 920 | this.saveGame(room, result); 921 | 922 | if (room.hasOnlinePlayer() || room.spectators.length > 0) // check room isn't empty 923 | try{ // users can leave room and room will be closed before round result send 924 | this.server.router.send({ 925 | module: 'game_manager', 926 | type: 'round_end', 927 | target: room, 928 | data: result 929 | }); 930 | } catch (e) { 931 | logger.err('GameManager.sendGameResult, err:', e, 1); 932 | } 933 | else { 934 | logger.warn('GameManager.sendGameResult, room:', room.id, 'no players online', 1); 935 | } 936 | 937 | if (callback) callback(); 938 | for (i = 0; i < room.players.length; i++) { 939 | if (!room.players[i].isConnected && !room.players[i].isRemoved) this.server.onUserLeave(room.players[i]); 940 | } 941 | }; 942 | 943 | 944 | GameManager.prototype.saveGame = function(room, result){ 945 | if (!result.save || !room.saveHistory) return; 946 | var save = {}, game = room.game, userData = {}; 947 | for (var key in result){ 948 | if (result.hasOwnProperty(key) && ['score', 'ratings', 'save', 'saveHistory', 'saveRating'].indexOf(key) == -1){ 949 | save[key] = result[key]; 950 | } 951 | } 952 | save.roomId = room.id; 953 | save.mode = room.mode; 954 | save.history = game.shistory; 955 | save.players = game.initData.players; 956 | game.initData.players = undefined; 957 | game.initData.score = undefined; 958 | try{ 959 | save.initData = JSON.stringify(game.initData); 960 | } catch (e){ 961 | logger.err('GameManager.saveGame initData, error: ', e, 1); 962 | save.initData = 'error'; 963 | } 964 | try{ 965 | save.score = JSON.stringify(result.score); 966 | } catch (e){ 967 | logger.err('GameManager.saveGame score, error: ', e, 1); 968 | save.score = 'error'; 969 | } 970 | try{ 971 | for (var i = 0; i < room.players.length; i++) 972 | userData[room.players[i].userId] = room.players[i].getInfo(room.mode); 973 | save.userData = JSON.stringify(userData); 974 | } catch (e){ 975 | logger.err('GameManager.saveGame userData, error: ', e, 1); 976 | save.userData = 'error'; 977 | } 978 | logger.log('GameManager.saveGame, save: ', save.roomId, 'time: ', Date.now() - save.timeEnd, 3); 979 | this.server.storage.pushGame(save); 980 | }; 981 | 982 | 983 | GameManager.prototype.getUserRoom = function(user, notSpectator){ 984 | notSpectator = notSpectator !== false; // true default 985 | if (!user.currentRoom) return null; 986 | if (!notSpectator) return user.currentRoom; 987 | if (user.currentRoom.players.indexOf(user) != -1) return user.currentRoom; 988 | else { 989 | logger.log('GameManager.getUserRoom', 'user spectate in', user.currentRoom.id, user.userId, 3); 990 | return null; 991 | } 992 | }; 993 | 994 | 995 | GameManager.prototype.getSpectatorRoom = function (user){ 996 | if (!user.currentRoom) return null; 997 | if (user.currentRoom.spectators.indexOf(user) != -1) return user.currentRoom; 998 | else { 999 | logger.err('GameManager.getSpectatorRoom', 'user not spectate in', user.currentRoom.id, user.userId, 1); 1000 | return null; 1001 | } 1002 | }; 1003 | 1004 | 1005 | GameManager.prototype.sendError = function(user, error){ 1006 | this.server.router.send({ 1007 | module: 'game_manager', 1008 | type: 'error', 1009 | target: user, 1010 | data: error 1011 | }); 1012 | }; 1013 | 1014 | 1015 | GameManager.prototype.generateRoomId = function(owner, type){ 1016 | //game format name: "game_type_userId_socketId_hh.mm.ss" 1017 | var now = new Date(); 1018 | return this.server.game + "_" + type + "_" + owner.userId + "_" + owner.socket.id 1019 | + "_" + now.getHours() + ":" + now.getMinutes() + ":" + now.getSeconds(); 1020 | }; -------------------------------------------------------------------------------- /lib/history_manager.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var logger = require('./logger.js'); 3 | 4 | module.exports = HistoryManager; 5 | 6 | function HistoryManager(server){ 7 | 8 | var self = this; 9 | this.server = server; 10 | 11 | } 12 | 13 | 14 | HistoryManager.prototype.onMessage = function(message, type){ 15 | var data = message.data; 16 | switch (type) {//TODO: check mode isn't wrong, userId 17 | case 'history': // load history 18 | this.loadHistory(message.sender, data.userId||message.sender.userId, data.mode, data.count, data.offset, data.filter); 19 | break; 20 | case 'game': // load game history and params 21 | this.loadGame(message.sender, data.userId||message.sender.userId, data.id, data.mode); 22 | break; 23 | } 24 | }; 25 | 26 | 27 | HistoryManager.prototype.loadHistory = function(sender, userId, mode, count, offset, filter){ 28 | if (!userId || !mode){ 29 | logger.err('HistoryManager.loadHistory ', 'wrong arguments', userId, mode, 1); 30 | return; 31 | } 32 | var self = this; 33 | this.server.storage.getHistory(userId, mode, count, offset, filter) 34 | .then(function(results){ 35 | try { 36 | self.server.router.send({ 37 | module: 'history_manager', 38 | type: 'history', 39 | target: sender, 40 | data: { 41 | mode: mode, 42 | history: results[0], 43 | penalties: results[1], 44 | userId: userId 45 | } 46 | } 47 | ); 48 | } catch (err) { 49 | logger.err('HistoryManager.loadHistory error', err, 1); 50 | self.sendEmptyHistory(sender, mode, userId); 51 | } 52 | }) 53 | .catch(function(err){ 54 | logger.err('HistoryManager.loadHistory error', err, 1); 55 | self.sendEmptyHistory(sender, mode, userId) 56 | }); 57 | }; 58 | 59 | HistoryManager.prototype.sendEmptyHistory = function(sender, mode, userId) { 60 | this.server.router.send({ 61 | module: 'history_manager', 62 | type: 'history', 63 | target: sender, 64 | data: { 65 | mode: mode, 66 | history:[], 67 | penalties:null, 68 | userId:userId 69 | } 70 | } 71 | ); 72 | }; 73 | 74 | 75 | 76 | HistoryManager.prototype.loadGame = function(sender, userId, id, mode){ 77 | if (!userId || !id){ 78 | logger.err('HistoryManager.loadGame ', 'wrong arguments', userId, id, 2); 79 | return; 80 | } 81 | var self = this; 82 | this.server.storage.getGame(userId, id, mode) 83 | .then(function(game){ 84 | self.server.router.send({ 85 | module: 'history_manager', 86 | type: 'game', 87 | target: sender, 88 | data: { 89 | mode: mode, 90 | game: game 91 | } 92 | } 93 | ); 94 | }) 95 | .catch(function(err){ 96 | logger.err('HistoryManager.loadGame error', err, 1); 97 | self.server.router.send({ 98 | module: 'history_manager', 99 | type: 'game', 100 | target: sender, 101 | data: { 102 | mode: mode, 103 | game: null 104 | } 105 | } 106 | ); 107 | }); 108 | }; -------------------------------------------------------------------------------- /lib/invite_manager.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var util = require('util'); 3 | var logger = require('./logger.js'); 4 | 5 | module.exports = InviteManager; 6 | 7 | function InviteManager(server){ 8 | EventEmitter.call(this); 9 | 10 | this.server = server; 11 | this.invites = {}; 12 | this.waiting = {}; 13 | 14 | this.server.on("user_leave", function(user){ 15 | this.removeWaitingUser(user); 16 | if (this.invites[user.userId]) delete this.invites[user.userId] 17 | }.bind(this)); 18 | 19 | } 20 | 21 | util.inherits(InviteManager, EventEmitter); 22 | 23 | 24 | InviteManager.prototype.onMessage = function(message, type){ 25 | switch (type){ 26 | case "invite": this.onInvite(message); break; 27 | case "cancel": this.onInviteCancel(message); break; 28 | case "accept": this.onInviteAccepted(message); break; 29 | case "reject": this.onInviteRejected(message); break; 30 | case "random": this.onPlayRandom(message.sender, message.data); break; 31 | } 32 | }; 33 | 34 | 35 | InviteManager.prototype.onInvite = function(invite){ 36 | // TODO: check invite can be send 37 | if (invite.target == invite.sender.userId){ 38 | logger.warn('InviteManager.onInvite', 'user invite himself', invite.target, 1); 39 | return; 40 | } 41 | invite.data = invite.data || {}; 42 | invite.data.mode = invite.data.mode || this.server.modes[0]; 43 | invite.data.from = invite.sender.userId; 44 | var target = this.server.getUserById(invite.target); 45 | if (!target) { 46 | logger.warn('InviteManager.onInvite', 'no user', invite.target, 2); 47 | return; 48 | } 49 | if (this.server.gameManager.getUserRoom(invite.sender) || this.server.gameManager.getUserRoom(target)){ 50 | logger.warn('InviteManager.onInvite', 'invite user already in room!', 2); 51 | return; 52 | } 53 | this.invites[invite.sender.userId] = invite.data; 54 | this.server.router.send({ 55 | module : "invite_manager", 56 | type: "invite", 57 | sender:invite.sender, 58 | target:target, 59 | data:invite.data 60 | }); 61 | }; 62 | 63 | 64 | InviteManager.prototype.onInviteCancel = function(invite){ 65 | var target = this.server.getUserById(invite.target); 66 | invite.data.from = invite.sender.userId; 67 | delete this.invites[invite.data.form]; 68 | if (!target) { 69 | logger.warn('InviteManager.onInviteCancel', 'no user', invite.target, 2); 70 | return; 71 | } 72 | this.server.router.send({ 73 | module : "invite_manager", 74 | type: "cancel", 75 | sender:invite.sender, 76 | target: target, 77 | data:invite.data 78 | }); 79 | }; 80 | 81 | 82 | InviteManager.prototype.onInviteAccepted = function(invite){ 83 | var target = this.server.getUserById(invite.target); 84 | if (!target) { 85 | logger.warn('InviteManager.onInviteAccepted', 'no user', invite.target, 1); 86 | return; 87 | } 88 | if (this.server.gameManager.getUserRoom(invite.sender)){ 89 | logger.log('InviteManager.onInviteAccepted', 'accepted user already in room', invite.sender.userId, 2); 90 | this.server.router.send({ 91 | module : "invite_manager", 92 | type: "reject", 93 | sender: target, 94 | target: target, 95 | data: invite.data 96 | }); 97 | return; 98 | } 99 | if (this.server.gameManager.getUserRoom(target)){ 100 | logger.log('InviteManager.onInviteAccepted', 'sent user already in room', target.userId, 2); 101 | return; 102 | } 103 | 104 | if (!this.invites[target.userId]){ 105 | logger.warn('InviteManager.onInviteAccepted', 'invite not exists, invite sender and accepted:', target.userId, target.isConnected, invite.sender.userId, invite.sender.isConnected, 1); 106 | this.server.router.send({ 107 | module : "invite_manager", 108 | type: "reject", 109 | sender: target, 110 | target: target, 111 | data: invite.data 112 | }); 113 | return; 114 | } 115 | 116 | // check waiting user 117 | this.removeWaitingUser(target); 118 | this.removeWaitingUser(invite.sender); 119 | 120 | this.emit("invite_accepted", { 121 | owner:target, 122 | players:[target, invite.sender], 123 | data: this.invites[target.userId] 124 | }); 125 | delete this.invites[target.userId]; 126 | }; 127 | 128 | 129 | InviteManager.prototype.onInviteRejected = function(invite){ 130 | invite.data = invite.data || {}; 131 | var target = this.server.getUserById(invite.target); 132 | if (!target) { 133 | logger.warn('InviteManager.onInviteRejected', 'no user', invite.target, 1); 134 | return; 135 | } 136 | this.server.router.send({ 137 | module : "invite_manager", 138 | type: "reject", 139 | sender:invite.sender, 140 | target:target, 141 | data:invite.data 142 | }); 143 | delete this.invites[target.userId]; 144 | }; 145 | 146 | 147 | InviteManager.prototype.onPlayRandom = function(user, data){ 148 | logger.log('InviteManager.onRandomPlay', user?user.userId:null, data, 3); 149 | if (!user || !user.userId || !data) { 150 | logger.err('InviteManager.onRandomPlay wrong parameters', 1); 151 | return; 152 | } 153 | // remove and check turn off 154 | if (this.removeWaitingUser(user) == data.mode || data == 'off'){ 155 | return; 156 | } 157 | 158 | if (this.server.gameManager.getUserRoom(user)){ 159 | logger.warn('InviteManager', 'random play user already in room!', 1); 160 | return; 161 | } 162 | 163 | var wrongMode = true; 164 | for (var i = 0; i < this.server.modes.length; i++){ 165 | if (this.server.modes[i] == data.mode) { 166 | wrongMode = false; 167 | break; 168 | } 169 | } 170 | 171 | if (wrongMode){ 172 | logger.err('InviteManager.onRandomPlay wrong game mode ', data.mode, 1); 173 | return; 174 | } 175 | 176 | if (this.waiting[data.mode]){ // there is user waiting random play 177 | if (this.server.gameManager.getUserRoom(this.waiting[data.mode])){ 178 | logger.warn('InviteManager', 'waiting user already in room!', 1); 179 | this.waiting[data.mode] = user; 180 | return; 181 | } 182 | logger.log('InviteManager.onRandomPlay, start game', user.userId, this.waiting[data.mode].userId, 2); 183 | this.emit("invite_accepted", { 184 | owner: user, 185 | players:[user, this.waiting[data.mode]], 186 | data: data 187 | }); 188 | this.waiting[data.mode] = null; 189 | } else { 190 | this.waiting[data.mode] = user; 191 | var sendData = {}; 192 | sendData[data.mode] = user.userId; 193 | this.server.router.send({ 194 | module : "invite_manager", 195 | type: "random_wait", 196 | target: this.server.game, 197 | data: sendData 198 | }); 199 | } 200 | }; 201 | 202 | 203 | InviteManager.prototype.removeWaitingUser = function(user){ 204 | for (var i = 0; i < this.server.modes.length; i++){ 205 | if (this.waiting[this.server.modes[i]] == user){ 206 | this.waiting[this.server.modes[i]] = null; 207 | logger.log('InviteManager.removeWaitingUser ', user.userId, this.server.modes[i], 3); 208 | var sendData = {}; 209 | sendData[this.server.modes[i]] = null; 210 | this.server.router.send({ 211 | module : "invite_manager", 212 | type: "random_cancel", 213 | target: this.server.game, 214 | data: sendData 215 | }); 216 | return this.server.modes[i]; 217 | } 218 | } 219 | return false; 220 | }; 221 | 222 | 223 | InviteManager.prototype.getWaitingUsers = function(){ 224 | var waiting = {}; 225 | for (var i = 0; i < this.server.modes.length; i++){ 226 | if (this.waiting[this.server.modes[i]]){ 227 | waiting[this.server.modes[i]] = this.waiting[this.server.modes[i]].userId; 228 | } 229 | } 230 | return waiting; 231 | }; -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | var Logger = {}; 2 | Logger.logLevel = 1; 3 | 4 | Logger._log = function() { 5 | var level = Array.prototype.pop.apply(arguments); 6 | Array.prototype.unshift.call(arguments, getTime()); 7 | if (level <= this.logLevel) { 8 | console.log.apply(console, arguments); 9 | } 10 | }; 11 | 12 | Logger.log = function() { 13 | Array.prototype.unshift.call(arguments, '- log;'); 14 | Logger._log.apply(Logger, arguments); 15 | }; 16 | 17 | 18 | Logger.err = function() { 19 | Array.prototype.unshift.call(arguments, '- err;'); 20 | Logger._log.apply(Logger, arguments); 21 | }; 22 | 23 | 24 | Logger.warn = function() { 25 | Array.prototype.unshift.call(arguments, '- wrn;'); 26 | Logger._log.apply(Logger, arguments); 27 | }; 28 | 29 | function getTime() { 30 | var month_names_short = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], 31 | time = '', d = new Date() ,t; 32 | t = d.getDate(); 33 | time += (t < 10 ? '0' : '') + t; 34 | t = month_names_short[d.getMonth()]; 35 | time += ' ' + t; 36 | t = d.getHours(); 37 | time += ' '+(t < 10 ? '0' : '') + t; 38 | t = d.getMinutes(); 39 | time += ':'+(t < 10 ? '0' : '') + t; 40 | t = d.getSeconds(); 41 | time += ':'+(t < 10 ? '0' : '') + t; 42 | 43 | return time; 44 | } 45 | 46 | module.exports = Logger; -------------------------------------------------------------------------------- /lib/mongo.js: -------------------------------------------------------------------------------- 1 | var MongoClient = require('mongodb').MongoClient; 2 | var ObjectId = require('mongodb').ObjectID; 3 | var util = require('util'); 4 | var logger = require('./logger.js'); 5 | 6 | module.exports = Mongo; 7 | 8 | function Mongo(server){ 9 | this.server = server; 10 | var conf = server.conf.mongo; 11 | conf.database = server.game; 12 | this.url = 'mongodb://' + conf.host + ':' + conf.port + '/' + conf.database; 13 | this.collections = [ 14 | { 15 | name: 'users', 16 | indexes:[ 17 | { 18 | name: 'userId', 19 | fields: {userId: 1}, 20 | unique: true 21 | }, 22 | { 23 | name: 'games', 24 | fields: {games: 1, ratingElo: 1}, 25 | mode: true 26 | }, 27 | { 28 | name: 'dateCreate', 29 | fields: {dateCreate: -1, games: 1}, 30 | mode: true 31 | }, 32 | { 33 | name: 'ratingElo', 34 | fields: {ratingElo: -1, games: 1}, 35 | mode: true 36 | }, 37 | { 38 | name: 'win', 39 | fields: {win: -1, games: 1}, 40 | mode: true 41 | }, 42 | { 43 | name: 'timeLastGame', 44 | fields: {timeLastGame: -1}, 45 | mode: true 46 | } 47 | ] 48 | }, 49 | { 50 | name: 'settings', 51 | indexes:[ 52 | { 53 | name: 'userId', 54 | fields: {userId: 1}, 55 | unique: true 56 | } 57 | ] 58 | }, 59 | { 60 | name: 'history', 61 | indexes:[ 62 | { 63 | name: 'players', 64 | fields: {players: 1, mode: 1, timeEnd: -1} 65 | } 66 | ] 67 | }, 68 | { name: 'games', indexes:[] }, 69 | { 70 | name: 'messages', 71 | indexes:[ 72 | { 73 | name: 'userId', 74 | fields: {userId: 1, time: -1} 75 | }, 76 | { 77 | name: 'target', 78 | fields: {target: 1, time: -1} 79 | }, 80 | { 81 | name: 'time', 82 | fields: {time: -1} 83 | }, 84 | { 85 | name: 'target_userId', 86 | fields: {target: 1, userId: 1, time: -1} 87 | } 88 | ] 89 | }, 90 | { 91 | name: 'bans', 92 | indexes:[ 93 | { 94 | name: 'userId', 95 | fields: {userId: 1, timeEnd: -1} 96 | } 97 | ] 98 | }, 99 | { 100 | name: 'penalties', 101 | indexes:[ 102 | { 103 | name: 'userId', 104 | fields: {userId: 1, mode: 1, time: -1} 105 | } 106 | ] 107 | } 108 | ] 109 | } 110 | 111 | Mongo.prototype.init = function(callback){ 112 | var self = this; 113 | MongoClient.connect(this.url, function(err, db) { 114 | if (err) { 115 | logger.err('Mongo', self.url, err, 1); 116 | throw new Error('mongo connect failed!'); 117 | } 118 | logger.log('Mongo.init', "Success connected to mongo ", self.url, 1); 119 | // create collections 120 | db.collection('users'); 121 | db.collection('settings'); 122 | db.collection('history'); 123 | db.collection('games'); 124 | db.collection('messages'); 125 | db.collection('bans'); 126 | var col = db.collection('test'); 127 | var key = Date.now(); 128 | col.insertOne({time: key}, function (err, resultInsert) { 129 | logger.log('Mongo.init test insert, error:', err, 1); 130 | if (!resultInsert) { 131 | throw new Error('test insert failed!'); 132 | } 133 | if (resultInsert.insertedCount == 1) { 134 | col.findOne({time: key}, function (err2, resultFind) { 135 | db.close(); 136 | logger.log('log;', 'Mongo.init test find, error:', err2, 'result: ', resultFind, 1); 137 | logger.log('log;', 'Mongo.init test total, error: ', err2, 'result: ', resultFind ? 'complete' : 'failed', 1); 138 | self.createIndexNotExists('test', 'time', {time: -1}).then(function(index) { 139 | logger.log('log;', 'Mongo.init test index result: ', index == 'time' ? 'complete' : 'failed', 1); 140 | if (callback) callback(); 141 | }); 142 | }); 143 | } else { 144 | db.close(); 145 | if (callback) callback(); 146 | } 147 | }); 148 | }); 149 | }; 150 | 151 | Mongo.prototype.connect = function(){ 152 | var self = this; 153 | return new Promise(function(res, rej){ 154 | MongoClient.connect(self.url, function(err, db) { 155 | if (err) { 156 | logger.err('Mongo connect failed', err, 1); 157 | rej(err); 158 | return; 159 | } 160 | res(db); 161 | }); 162 | }); 163 | }; 164 | 165 | 166 | Mongo.prototype.getUserData = function (userId) { 167 | var self = this; 168 | return new Promise(function(res, rej){ 169 | self.connect() 170 | .then(function (db){ 171 | var col = db.collection('users'); 172 | col.findOne({userId: userId},{}, function (err, item){ 173 | db.close(); 174 | if (err){ 175 | logger.err('Mongo.getUserData', err, 1); 176 | rej(err); 177 | return; 178 | } 179 | item = item || {userId: userId}; 180 | res(item); 181 | }); 182 | }) 183 | .catch(function(err){ 184 | rej(err); 185 | }); 186 | }); 187 | }; 188 | 189 | 190 | Mongo.prototype.saveUserData = function (userData){ 191 | var self = this; 192 | return new Promise(function(res, rej){ 193 | self.connect() 194 | .then(function (db){ 195 | var col = db.collection('users'); 196 | col.updateOne({userId: userData.userId}, userData, {upsert:true, w: 1}, function (err, result){ 197 | db.close(); 198 | if (err){ 199 | logger.err('Mongo.saveUserData error', err, 1); 200 | rej(err); 201 | return; 202 | } 203 | logger.log('Mongo.saveUserData user updated', userData.userId, 204 | result.matchedCount, 205 | result.modifiedCount, 206 | result.upsertedId, 207 | result.upsertedCount, 3); 208 | res(true); 209 | }); 210 | }) 211 | .catch(function(err){ 212 | rej(err); 213 | }); 214 | }); 215 | }; 216 | 217 | 218 | Mongo.prototype.getUserSettings = function (userId) { 219 | var self = this; 220 | return new Promise(function(res, rej){ 221 | self.connect() 222 | .then(function (db){ 223 | var col = db.collection('settings'); 224 | col.findOne({userId: userId},{}, function (err, item){ 225 | db.close(); 226 | if (err){ 227 | logger.err('Mongo.getUserSettings', err, 1); 228 | } 229 | res(item?item.settings:null); 230 | }); 231 | }) 232 | .catch(function(err){ 233 | rej(err); 234 | }); 235 | }); 236 | }; 237 | 238 | 239 | Mongo.prototype.saveUserSettings = function (userId, settings){ 240 | var self = this; 241 | return new Promise(function(res, rej){ 242 | self.connect() 243 | .then(function (db){ 244 | var col = db.collection('settings'); 245 | col.updateOne({userId: userId}, {userId: userId, settings: settings}, {upsert:true, w: 1}, function (err, result){ 246 | db.close(); 247 | if (err){ 248 | logger.err('Mongo.saveUserSettings error', err, 1); 249 | rej(err); 250 | return; 251 | } 252 | logger.log('log;', 'Mongo.saveUserSettings user updated', userId, 253 | result.matchedCount, 254 | result.modifiedCount, 255 | result.upsertedId, 256 | result.upsertedCount, 3); 257 | res(true); 258 | }); 259 | }) 260 | .catch(function(err){ 261 | rej(err); 262 | }); 263 | }); 264 | }; 265 | 266 | 267 | Mongo.prototype.saveMessage = function (message) { 268 | var self = this; 269 | self.connect() 270 | .then(function (db){ 271 | var col = db.collection('messages'); 272 | col.insertOne(message, function(err, result){db.close();}); 273 | }) 274 | .catch(function(err){}); 275 | }; 276 | 277 | 278 | Mongo.prototype.deleteMessage = function (id) { 279 | var self = this; 280 | self.connect() 281 | .then(function (db){ 282 | var col = db.collection('messages'); 283 | col.deleteOne({time:id}, function(err, result){db.close();}); 284 | }) 285 | .catch(function(err){}); 286 | }; 287 | 288 | 289 | Mongo.prototype.loadMessages = function (count, time, target, sender){ 290 | var self = this, timeStart = Date.now(); 291 | return new Promise(function(res, rej){ 292 | self.connect() 293 | .then(function (db){ 294 | var col = db.collection('messages'); 295 | var query = { 296 | time:{$lt:time} 297 | }; 298 | if (!sender) // public 299 | query.target = target; 300 | else // private 301 | query.$or = [{target: target, userId:sender}, {target:sender, userId:target}]; 302 | col.find(query, { "sort": [['time','desc']], limit:count}).toArray(function (err, items){ 303 | logger.log('Mongo.loadMessages query: db.messages.find(', query, ').sort({time: -1}) time: ', Date.now()-timeStart , 3); 304 | db.close(); 305 | if (err){ 306 | logger.err('Mongo.loadMessages', err, 1); 307 | rej(err); 308 | return; 309 | } 310 | res(items); 311 | }); 312 | }) 313 | .catch(function(err){ 314 | rej(err); 315 | }); 316 | }); 317 | }; 318 | 319 | 320 | Mongo.prototype.saveBan = function (ban){ 321 | var self = this; 322 | self.connect() 323 | .then(function (db){ 324 | var col = db.collection('bans'); 325 | col.insertOne(ban, function(err, result){db.close();}); 326 | }) 327 | .catch(function(err){}); 328 | }; 329 | 330 | 331 | Mongo.prototype.loadBan = function (userId){ 332 | var self = this; 333 | return new Promise(function(res, rej){ 334 | self.connect() 335 | .then(function (db){ 336 | var col = db.collection('bans'); 337 | col.findOne({ userId: userId, timeEnd:{'$gt': Date.now()} }, function(err, item){ 338 | db.close(); 339 | if (err){ 340 | logger.err('Mongo.loadBan', err, item, 1); 341 | } 342 | res(item); 343 | }); 344 | }) 345 | .catch(function(err){}); 346 | }); 347 | 348 | }; 349 | 350 | 351 | Mongo.prototype.loadRanks = function (mode, count, skip, ratingElo) { 352 | var self = this, timeStart = Date.now(); 353 | skip = skip || 0; 354 | ratingElo = ratingElo || 1600; 355 | return new Promise(function(res, rej){ 356 | self.connect() 357 | .then(function (db){ 358 | var col = db.collection('users'); 359 | var query = {}; 360 | query[mode+'.games'] = {'$gt': 0}; 361 | query[mode+'.ratingElo'] = {'$gte': 0}; 362 | col.find(query, {skip: skip, limit:count}).toArray(function (err, items){ 363 | logger.log('Mongo.loadRanks query: db.users.find(', query, ') time: ', Date.now()-timeStart, 3); 364 | db.close(); 365 | if (err){ 366 | logger.err('Mongo.loadRanks', err, 1); 367 | rej(err); 368 | return; 369 | } 370 | res({ 371 | items: items, 372 | skip: skip 373 | }); 374 | }); 375 | }) 376 | .catch(function(err){ 377 | rej(err); 378 | }); 379 | }); 380 | }; 381 | 382 | 383 | Mongo.prototype.loadRating = function(mode, count, skip, column, order, filter){ 384 | var self = this, timeStart = Date.now(); 385 | if (column != 'dateCreate') column = mode + '.' + column; 386 | order = order || 'desc'; 387 | return new Promise(function(res, rej){ 388 | self.connect() 389 | .then(function (db){ 390 | var col = db.collection('users'); 391 | var query = {}; 392 | query[mode+'.games'] = { '$gt': 0 }; 393 | if (filter) query['userName'] = {$regex: '^' + filter, $options: 'i'}; 394 | col.find(query, {"sort": [[column, order]], skip: skip, limit:count}).toArray(function (err, allUsers){ 395 | db.close(); 396 | if (err){ 397 | logger.err('Mongo.loadRating', err, 1); 398 | rej(err); 399 | } else { 400 | logger.log('Mongo.loadRating query: db.users.find(', query, ').sort({"', column,'" : 1}) time: ', Date.now()-timeStart, 3); 401 | res(allUsers); 402 | } 403 | }); 404 | }) 405 | .catch(function(err){ 406 | rej(err); 407 | }); 408 | }); 409 | }; 410 | 411 | 412 | Mongo.prototype.saveGame = function(save){ 413 | var self = this; 414 | self.connect() 415 | .then(function(db){ 416 | var col = db.collection('games'); 417 | col.insertOne(save, function(err, result){ 418 | if (err || result.insertedCount != 1){ 419 | db.close(); 420 | logger.err('Mongo.saveGame', err || result, 1); 421 | return; 422 | } 423 | logger.log('Mongo.saveGame game saved ',result.insertedId, 3); 424 | var game = { 425 | _id: result.insertedId, 426 | timeStart: save.timeStart, 427 | timeEnd: save.timeEnd, 428 | players: save.players, 429 | mode: save.mode, 430 | winner: save.winner, 431 | action: save.action, 432 | userData: save.userData 433 | }; 434 | col = db.collection('history'); 435 | col.insertOne(game, function(err, result){ 436 | db.close(); 437 | logger.log('Mongo.saveGame history saved', 3); 438 | }); 439 | }); 440 | }) 441 | .catch(function(err){}); 442 | }; 443 | 444 | 445 | Mongo.prototype.loadHistory = function(userId, mode, count, offset, filter){ 446 | var self = this, timeStart = Date.now(); 447 | return new Promise(function(res, rej){ 448 | self.connect() 449 | .then(function (db){ 450 | var col = db.collection('history'); 451 | var query = {players: {$in:[userId]}, mode: mode}; 452 | if (filter) query['userData'] = {$regex: '"userName":"'+filter, $options: 'i'}; 453 | col.find(query, { "sort": [['timeEnd', 'desc']], limit:count, skip: offset}).toArray(function (err, items){ 454 | db.close(); 455 | if (err){ 456 | logger.err('Mongo.loadHistory', err, 1); 457 | rej(err); 458 | return; 459 | } 460 | logger.log('Mongo.loadHistory query: db.history.find(', query, ').sort({timeEnd : -1}) time: ', Date.now()-timeStart, 3); 461 | res(items); 462 | }); 463 | }) 464 | .catch(function(err){ 465 | rej(err); 466 | }); 467 | }); 468 | }; 469 | Mongo.prototype.loadPenalties = function(userId, mode, timeStart, timeEnd){ 470 | var self = this, qtimeStart = Date.now(); 471 | return new Promise(function(res, rej){ 472 | if (!self.server.conf.penalties){ 473 | res(null); 474 | return; 475 | } 476 | self.connect() 477 | .then(function (db){ 478 | var col = db.collection('penalties'); 479 | var query = {userId: userId, mode: mode}; 480 | col.find(query, { "sort": [['time', 'desc']]}).toArray(function (err, items){ 481 | db.close(); 482 | if (err){ 483 | logger.err('Mongo.loadPenalties', err, 1); 484 | rej(err); 485 | return; 486 | } 487 | logger.log('Mongo.loadPenalties query: db.penalties.find(', query, ').sort({time : -1}) time: ', Date.now()-qtimeStart, 3); 488 | res(items); 489 | }); 490 | }) 491 | .catch(function(err){ 492 | rej(err); 493 | }); 494 | }); 495 | }; 496 | 497 | 498 | Mongo.prototype.loadGame = function(gameId){ 499 | var self = this; 500 | return new Promise(function(res, rej){ 501 | self.connect() 502 | .then(function (db){ 503 | var col = db.collection('games'); 504 | col.findOne({_id: new ObjectId(gameId)}, {}, function (err, item){ 505 | db.close(); 506 | if (err){ 507 | logger.err('Mongo.loadGame', err, 1); 508 | rej(err); 509 | return; 510 | } 511 | res(item); 512 | }); 513 | }) 514 | .catch(function(err){ 515 | rej(err); 516 | }); 517 | }); 518 | }; 519 | 520 | 521 | Mongo.prototype.createIndexes = function(){ 522 | var self = this; 523 | logger.log('Mongo.createIndexes', 'start', 2); 524 | var indexPromises = []; 525 | for (var i = 0; i < self.collections.length; i++){ 526 | for (var j = 0; j < self.collections[i].indexes.length; j++){ 527 | var index = self.collections[i].indexes[j]; 528 | var collection = self.collections[i].name; 529 | if (index.mode){ // generate fields for each game mode 530 | for (var n = 0; n < self.server.modes.length; n++){ 531 | var fields = {}, mode = self.server.modes[n]; 532 | for (var field in index.fields){ 533 | var name = field == 'dateCreate'?field:mode+'.'+field; 534 | fields[name] = index.fields[field]; 535 | } 536 | logger.log('Mongo.createIndexes', collection, mode+'_'+index.name, 'fields:', fields, 'unique:', !!index.unique, 3); 537 | indexPromises.push(self.createIndexNotExists(collection, mode+'_'+index.name, fields, !!index.unique)); 538 | } 539 | } else { 540 | logger.log('Mongo.createIndexes', collection, index.name, 'fields:', index.fields, 'unique:', !!index.unique, 3); 541 | indexPromises.push(self.createIndexNotExists(collection, index.name, index.fields, !!index.unique)); 542 | } 543 | } 544 | } 545 | return Promise.all(indexPromises); 546 | }; 547 | 548 | 549 | Mongo.prototype.createIndexNotExists = function (collection, name, fields, unique) { 550 | var self = this; unique = !!unique; 551 | return new Promise(function(res, rej){ 552 | self.connect() 553 | .then(function (db){ 554 | var col = db.collection(collection); 555 | col.indexExists(name, function (err, result){ 556 | if (err){ 557 | logger.err('Mongo.createIndexNotExists', collection, err, 2); 558 | db.close(); 559 | res(null); 560 | return; 561 | } 562 | if (result){ 563 | logger.log('Mongo.createIndexNotExists', name, 'exists', 3); 564 | db.close(); 565 | res(name); 566 | } else { 567 | logger.log('Mongo.createIndexNotExists', 'creating index', name, 3); 568 | col.createIndex(fields, {name: name, w: 1, unique: unique, dropDups: unique, background: true}, function(err, index){ 569 | db.close(); 570 | if (err){ 571 | logger.err('Mongo.createIndexNotExists', err, 1); 572 | } 573 | logger.log('log;', 'Mongo.createIndexNotExists', 'creating index', index, index?'complete':'failed', 3); 574 | res(index); 575 | }); 576 | } 577 | }); 578 | }) 579 | .catch(function(err){ 580 | rej(err); 581 | }); 582 | }); 583 | }; -------------------------------------------------------------------------------- /lib/rating_manager.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var math = require('math'); 3 | var logger = require('./logger.js'); 4 | 5 | module.exports = RatingManager; 6 | 7 | function RatingManager(server){ 8 | var self = this; 9 | this.server = server; 10 | this.timeLastUpdate = null; 11 | this.interval = server.conf.ratingUpdateInterval; 12 | this.updating = false; 13 | } 14 | 15 | 16 | RatingManager.prototype.onMessage = function(message, type){ 17 | var self = this, data; 18 | switch (type) { 19 | case 'ratings': // load history 20 | data = message.data; 21 | data.column = data.column || 'ratingElo'; 22 | data.order = data.order || 'desc'; 23 | data.mode = data.mode || this.server.modes[0]; 24 | this.server.storage.getRatings(message.sender.userId, data) 25 | .then(function(allUsers){ 26 | self.server.router.send({ 27 | module: 'rating_manager', 28 | type: 'ratings', 29 | target: message.sender, 30 | data: { 31 | mode: data.mode, 32 | column: data.column, 33 | order: data.order, 34 | ratings: { 35 | allUsers: allUsers, 36 | infoUser: message.sender.getInfo(data.mode), 37 | skip: +data.offset, 38 | offset: +data.offset 39 | } 40 | } 41 | } 42 | ); 43 | }) 44 | .catch(function(err){ // return null 45 | logger.err('RatingManager.loadRatings', err, 1); 46 | self.server.router.send({ 47 | module: 'rating_manager', 48 | type: 'ratings', 49 | target: message.sender, 50 | data: { 51 | mode: data.mode, 52 | column: data.column, 53 | order: data.order, 54 | ratings: { 55 | allUsers: [], 56 | infoUser: null 57 | } 58 | } 59 | } 60 | ); 61 | }); 62 | break; 63 | } 64 | }; 65 | 66 | 67 | RatingManager.prototype.computeNewRatings = function (room, result, callback){ 68 | logger.log( 'RatingManager.computeNewRatings', room.id,' compute: ', room.saveRating, 3); 69 | if (!room.saveRating) { 70 | callback(); 71 | return; 72 | } 73 | var mode = room.mode, winner, loser, self = this; 74 | if (result.winner == room.players[0].userId){ 75 | winner = room.players[0]; 76 | loser = room.players[1]; 77 | } else { 78 | winner = room.players[1]; 79 | loser = room.players[0]; 80 | } 81 | winner[mode]['games']++; 82 | loser[mode]['games']++; 83 | winner[mode]['timeLastGame'] = loser[mode]['timeLastGame'] = result.timeEnd; 84 | 85 | if (!result.winner) { 86 | winner[mode]['draw']++; 87 | loser[mode]['draw']++; 88 | if (this.server.conf.ratingElo && this.server.conf.calcDraw) this.computeNewElo(mode, winner, loser, true); 89 | 90 | } else { 91 | winner[mode]['win']++; 92 | loser[mode]['lose']++; 93 | if (this.server.conf.ratingElo) this.computeNewElo(mode, winner, loser); 94 | } 95 | 96 | this.server.storage.saveUsers(room.players, mode, function () { 97 | if (!self.updating && (!self.timeLastUpdate || !self.interval 98 | || Date.now() - self.timeLastUpdate > self.interval)) { 99 | self.updating = true; 100 | var startTime = Date.now(); 101 | self.server.storage.updateRatings(mode) 102 | .then(function(result){ 103 | self.updating = false; 104 | self.timeLastUpdate = Date.now(); 105 | logger.log('RatingManager.computeNewRatings', 'ranks updated', result.length, ' time: ',Date.now() - startTime, 2); 106 | }) 107 | .catch(function (err){ 108 | self.updating = false; 109 | logger.err('RatingManager.computeNewRatings', 'ranks updating failed', err, 1); 110 | }); 111 | } 112 | callback(); 113 | }); 114 | }; 115 | 116 | 117 | RatingManager.prototype.computeNewElo = function (mode, winner, loser, isDraw){ 118 | if (isDraw){ 119 | winner[mode]['ratingElo'] = RatingManager.eloCalculation(winner[mode]['ratingElo'], loser[mode]['ratingElo'], 0.5, winner[mode]['games']<30); 120 | loser[mode]['ratingElo'] = RatingManager.eloCalculation(loser[mode]['ratingElo'], winner[mode]['ratingElo'], 0.5, loser[mode]['games']<30); 121 | } else { 122 | winner[mode]['ratingElo'] = RatingManager.eloCalculation(winner[mode]['ratingElo'], loser[mode]['ratingElo'], 1, winner[mode]['games']<30); 123 | loser[mode]['ratingElo'] = RatingManager.eloCalculation(loser[mode]['ratingElo'], winner[mode]['ratingElo'], 0, loser[mode]['games']<30); 124 | } 125 | }; 126 | 127 | RatingManager.eloCalculation = function(player1Elo, player2Elo, sFaktor, isNovice) { 128 | var kFactor = 15; 129 | if(player1Elo >= 2400) kFactor = 10; 130 | else if(isNovice) kFactor = 30; 131 | var expectedScoreWinner = 1 / ( 1 + math.pow(10, (player2Elo - player1Elo)/400) ); 132 | var e = kFactor * (sFaktor - expectedScoreWinner); 133 | return player1Elo + ~~e; //round e 134 | }; -------------------------------------------------------------------------------- /lib/room.js: -------------------------------------------------------------------------------- 1 | var logger = require('./logger.js'); 2 | 3 | module.exports = Room; 4 | 5 | function Room(id, owner, players, data){ 6 | this.id = id; 7 | this.owner = owner; 8 | this.players = players; 9 | this.spectators = []; 10 | this.inviteData = data; 11 | this.mode = data.mode; 12 | this.timeout = null; 13 | this.games = 0; 14 | this.saveHistory = false; 15 | this.saveRating = false; 16 | this.turnTime = 0; 17 | this.corTime = 0; 18 | this.userTurnTime = null; 19 | this.createTime = Date.now(); 20 | 21 | //game data 22 | this.game = { 23 | state:"waiting", 24 | current: owner 25 | }; 26 | 27 | // players data 28 | this.data = {}; 29 | for (var i = 0; i < players.length; i++) { 30 | this.data[players[i].userId] = { 31 | ready: false, 32 | timeouts:0, 33 | takeBacks:0, 34 | win:0 35 | }; 36 | } 37 | } 38 | 39 | Room.prototype.name = '__Room__'; 40 | 41 | 42 | Room.prototype.getPlayersId = function(){ 43 | var ids = []; 44 | for (var i = 0; i < this.players.length; i++) ids.push(this.players[i].userId); 45 | return ids; 46 | }; 47 | 48 | Room.prototype.getSpectatorsId = function(){ 49 | var ids = []; 50 | for (var i = 0; i < this.spectators.length; i++) ids.push(this.spectators[i].userId); 51 | return ids; 52 | }; 53 | 54 | 55 | Room.prototype.getInfo = function(){ 56 | return { 57 | room: this.id, 58 | owner: this.owner.userId, 59 | data: this.inviteData, 60 | players: this.getPlayersId(), 61 | spectators: this.getSpectatorsId(), 62 | mode: this.mode, 63 | turnTime: this.turnTime, 64 | takeBacks: this.takeBacks, 65 | timeMode: this.timeMode, 66 | timeStartMode: this.timeStartMode 67 | }; 68 | }; 69 | 70 | 71 | Room.prototype.getOpponent = function(user) { 72 | if (!user || !user.userId){ 73 | throw new Error('wrong user to get opponent'); 74 | } 75 | return this.players[0] == user ? this.players[1] : this.players[0]; 76 | }; 77 | 78 | 79 | Room.prototype.getGameData = function() { 80 | return { 81 | roomInfo:this.getInfo(), 82 | initData:this.game.initData, 83 | state: this.game.state, 84 | score: this.getScore(), 85 | history: this.game.state == 'waiting' ? '' : this.game.shistory, // TODO: clear history on round end, after saving game 86 | nextPlayer: this.game.current.userId, 87 | userTime: this.timeout ? Date.now() - this.game.turnStartTime : null, 88 | playerTurns: this.game.playerTurns, 89 | turnTime: this.turnTime, 90 | gameTime: this.createTime ? Date.now() - this.createTime : 0, 91 | roundTime: this.game.timeStart ? Date.now() - this.game.timeStart: 0, 92 | takeBacks: this.takeBacks, 93 | saveHistory: this.saveHistory, 94 | saveRating: this.saveRating, 95 | usersTakeBacks: this.getUsersTakeBacks(), 96 | userData: this.getUserData() 97 | }; 98 | }; 99 | 100 | 101 | Room.prototype.getUserData = function(){ 102 | var data = {}, i, player, id; 103 | for (i = 0; i < this.players.length; i++){ 104 | player = this.players[i]; 105 | id = player.userId; 106 | if (!data[id]){ 107 | data[id] = { 108 | userTotalTime: this.data[id].userTotalTime, 109 | userTurnTime: this.data[id].userTurnTime, 110 | takeBacks: this.data[id].takeBacks, 111 | win: this.data[id].win 112 | } 113 | } 114 | } 115 | return data; 116 | }; 117 | 118 | 119 | Room.prototype.getScore = function (){ 120 | var score = { 121 | games: this.games 122 | }; 123 | for (var i = 0; i < this.players.length; i++) 124 | score[this.players[i].userId] = this.data[this.players[i].userId].win; 125 | return score; 126 | }; 127 | 128 | 129 | Room.prototype.getUsersTakeBacks = function(){ 130 | var usersTakeBacks = {}; 131 | for (var i = 0; i < this.players.length; i++) 132 | usersTakeBacks[this.players[i].userId] = this.data[this.players[i].userId].takeBacks; 133 | return usersTakeBacks; 134 | }; 135 | 136 | 137 | Room.prototype.setUserTurnTime = function(time, user){ 138 | if (user) { 139 | this.data[user.userId].userTurnTime = time; 140 | } else { 141 | for (var i = 0; i < this.players.length; i++){ 142 | this.data[this.players[i].userId].userTurnTime = time; 143 | } 144 | } 145 | }; 146 | 147 | 148 | Room.prototype.getTurnTime = function(user){ 149 | if (!user) user = this.game.current; 150 | return this.data[user.userId].userTurnTime; 151 | }; 152 | 153 | 154 | Room.prototype.getUserTime = function(){ 155 | return Date.now() - (this.game.turnStartTime || this.game.timeStart); 156 | }; 157 | 158 | 159 | Room.prototype.savePlayerTurn = function(turn, nextPlayer){ 160 | turn.userTurnTime = this.getTurnTime(nextPlayer); 161 | turn.userTime = this.getUserTime(); 162 | this.game.history.push(turn); 163 | try{ 164 | if (this.game.shistory.length>0) this.game.shistory += '@'; 165 | this.game.shistory += JSON.stringify(turn); 166 | } catch (e){ 167 | logger.err('Room.savePlayerTurn', 'json stringify error!', turn, e, 1) 168 | } 169 | }; 170 | 171 | 172 | Room.prototype.savePlayerEvent = function(target, event){ 173 | if (target.name == '__User__') 174 | event.target = target.userId; 175 | else event.target = 'room'; 176 | 177 | this.savePlayerTurn(event); 178 | }; 179 | 180 | 181 | Room.prototype.checkPlayersReady = function(){ 182 | for (var i = 0; i < this.players.length; i++){ 183 | if (this.data[this.players[i].userId].ready == false) return false; 184 | } 185 | return true; 186 | }; 187 | 188 | Room.prototype.isPlaying = function(){ 189 | // game started if there is timeout 190 | return this.game.state == "playing" && this.game && this.timeout; 191 | }; 192 | 193 | 194 | Room.prototype.hasOnlinePlayer = function(){ 195 | for (var i = 0; i < this.players.length; i++) 196 | if (this.players[i].isConnected) return true; 197 | return false; 198 | }; 199 | 200 | /** 201 | * game states: 202 | * waiting - waiting users ready 203 | * playing - users play 204 | * end - game round end 205 | */ -------------------------------------------------------------------------------- /lib/router.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var util = require('util'); 3 | var logger = require('./logger.js'); 4 | 5 | module.exports = Router; 6 | 7 | function Router(server){ 8 | EventEmitter.call(this); 9 | 10 | var self = this; 11 | this.server = server; 12 | this.wss = server.wss; 13 | 14 | // bind events 15 | this.wss.on('connection', function(socket){ 16 | logger.log('Router', "new socket_connection", socket.id, socket.cookie._userId, 3); 17 | 18 | socket.on('disconnect', function(reason){ 19 | if (reason == 'timeout') { 20 | self.emit('socket_timeout', socket); 21 | } else { 22 | self.emit('socket_disconnected', socket); 23 | } 24 | }); 25 | socket.on('message', function(message){ 26 | self.onSocketMessage(this, message); 27 | }); 28 | self.emit('socket_connection', socket); 29 | }); 30 | 31 | this.server.on('user_login', function(user){ 32 | self.send({ 33 | module:"server", type: "user_login", sender:user, target:self.server.game, data:user.getInfo() 34 | }); 35 | }); 36 | this.server.on('user_relogin', function(user){ 37 | self.send({ 38 | module:"server", type: "user_relogin", sender:user, target:self.server.game, data:user.getInfo() 39 | }); 40 | }); 41 | this.server.on('user_leave', function(user){ 42 | var userRoom = self.server.gameManager.getUserRoom(user, true); 43 | if (self.wss.rooms[self.server.game] && (!userRoom || !userRoom.isPlaying())) 44 | self.send({ 45 | module:"server", type: "user_leave", target:self.server.game, data:user.userId 46 | }); 47 | }); 48 | } 49 | 50 | util.inherits(Router, EventEmitter); 51 | 52 | 53 | Router.prototype.onSocketMessage = function(socket, message){ 54 | if (typeof message.type != "string" || typeof message.module != "string" || !message.data || !message.target) { 55 | logger.warn('Router.onSocketMessage', 'wrong income message', message, 1); 56 | return; 57 | } 58 | if (message.type == 'login'){ 59 | message.sender = socket; 60 | } else { 61 | try { 62 | message.sender = this.server.getUserById(socket.id); 63 | if (!message.sender) { // something wrong, user not exists, but client send message 64 | logger.warn('Router.onSocketMessage', 'user not exists userId: ', socket.userId, 'type:', message.type, socket.id, 1); 65 | var user = this.server.router.getUser(socket.userId); // check other socket connected 66 | if (user) { 67 | logger.warn('Router.onSocketMessage', 'user other socket connected ', user.userId, user.socket.id, 1); 68 | } 69 | // close socket end send error 70 | this.send({ 71 | module:'server', 72 | type:'error', 73 | target: socket.id, 74 | data:'send_message' 75 | }); 76 | socket.close(); 77 | return; 78 | } 79 | } catch (err){ // get user or log error 80 | logger.warn('Router.onSocketMessage', 'get user error ', err, 0); 81 | return; 82 | } 83 | } 84 | switch (message.module) { 85 | case 'invite_manager': this.server.inviteManager.onMessage(message, message.type); break; 86 | case 'game_manager': this.server.gameManager.onMessage(message, message.type); break; 87 | case 'chat_manager': this.server.chatManager.onMessage(message, message.type); break; 88 | case 'history_manager': this.server.historyManager.onMessage(message, message.type); break; 89 | case 'rating_manager': this.server.ratingManager.onMessage(message, message.type); break; 90 | case 'server': this.server.onMessage(message, message.type); break; 91 | case 'admin': this.server.adminManager.onMessage(message, message.type); break; 92 | } 93 | }; 94 | 95 | 96 | Router.prototype.send = function(message){ 97 | if (!message.type || !message.module || !message.data || !message.target) { 98 | logger.err('wrong sent message', message, 1); 99 | return; 100 | } 101 | 102 | logger.log("Router.send", message.module, message.type, 3); 103 | 104 | var target = message.target, 105 | sender = message.sender; 106 | delete message.sender; 107 | delete message.target; 108 | if ((target.id == this.server.game || target == this.server.game) && !this.wss.rooms[this.server.game]){ 109 | logger.warn("Router.send", 'no users to receive', message.module, message.type, 1); 110 | return; 111 | } 112 | switch (target.name){ 113 | case '__Socket__': 114 | target.send(message); 115 | break; 116 | case '__User__': 117 | target.socket.send(message); 118 | break; 119 | case '__Room__': 120 | if (sender && sender.socket) sender.socket.in(target.id).send(message); 121 | else this.wss.in(target.id).broadcast(message); 122 | break; 123 | default: 124 | if (typeof target == "string") { 125 | if (sender && sender.socket) sender.socket.in(target).send(message); 126 | else this.wss.in(target).broadcast(message); 127 | } else throw new Error('wrong target! ' + target); 128 | } 129 | }; -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events').EventEmitter; 2 | var Router = require('./router.js'); 3 | var InviteManager = require('./invite_manager.js'); 4 | var RatingManager = require('./rating_manager.js'); 5 | var HistoryManager = require('./history_manager.js'); 6 | var GameManager = require('./game_manager.js'); 7 | var ChatManager = require('./chat_manager.js'); 8 | var User = require('./user.js'); 9 | var AdminManager = require('./admin_manager.js'); 10 | var defaultConf = require('./conf.js'); 11 | var defaultEngine = require('./engine.js'); 12 | var SocketServer = require('v6-ws').SocketServer; 13 | var util = require('util'); 14 | var logger = require('./logger.js'); 15 | 16 | module.exports = function (opts, engine) { 17 | if (typeof opts != "object" || (typeof engine != "object" && typeof engine != "function")) 18 | throw new Error("Conf and engine are required"); 19 | opts.game = opts.game || defaultConf.game; 20 | opts.path = opts.game; 21 | opts.modes = opts.modes || opts.gameModes || defaultConf.gameModes; 22 | opts.modesAlias = opts.modesAlias || defaultConf.modesAlias; 23 | opts.ratingElo = opts.ratingElo !== false; 24 | opts.calcDraw = opts.calcDraw || defaultConf.calcDraw; 25 | opts.loadRanksInRating = !!opts.loadRanksInRating; 26 | opts.closeOldConnection = opts.closeOldConnection !== false; 27 | opts.reconnectOldGame = opts.reconnectOldGame !== false; 28 | opts.spectateEnable = opts.spectateEnable !== false; 29 | opts.clearTimeouts = opts.clearTimeouts !== false; 30 | opts.loseOnLeave = opts.loseOnLeave || defaultConf.loseOnLeave; 31 | opts.turnTime = opts.turnTime || defaultConf.turnTime; 32 | opts.timeMode = opts.timeMode || defaultConf.timeMode; 33 | opts.timeStartMode = opts.timeStartMode || defaultConf.timeStartMode; 34 | opts.addTime = opts.addTime || defaultConf.addTime; 35 | opts.corTime = opts.corTime || defaultConf.corTime; 36 | opts.maxTimeouts = opts.maxTimeouts || defaultConf.maxTimeouts; 37 | opts.maxOfflineTimeouts = opts.maxOfflineTimeouts || opts.maxTimeouts; 38 | opts.minTurns = opts.minTurns || defaultConf.minTurns; 39 | opts.takeBacks = opts.takeBacks || defaultConf.takeBacks; 40 | opts.ratingUpdateInterval = opts.ratingUpdateInterval || defaultConf.ratingUpdateInterval; 41 | opts.adminList = opts.adminList || defaultConf.adminList; 42 | opts.adminPass = opts.adminPass || defaultConf.adminPass; 43 | opts.db = opts.db || defaultConf.db; 44 | opts.mongo = opts.mongo || defaultConf.mongo; 45 | opts.redis = opts.redis || defaultConf.redis; 46 | opts.enableIpGames = opts.enableIpGames || defaultConf.enableIpGames; 47 | opts.minUnfocusedTurns = opts.minUnfocusedTurns || defaultConf.minUnfocusedTurns; 48 | opts.minPerUnfocusedTurns = opts.minPerUnfocusedTurns || defaultConf.minPerUnfocusedTurns; 49 | 50 | if (Server.TIME_MODES.indexOf(opts.timeMode) == -1){ 51 | opts.timeMode = defaultConf.timeMode; 52 | } 53 | if (Server.TIME_START_MODES.indexOf(opts.timeStartMode) == -1){ 54 | opts.timeStartMode = defaultConf.timeStartMode; 55 | } 56 | 57 | logger.logLevel = opts.logLevel || 1; 58 | 59 | return new Server(opts, engine); 60 | }; 61 | 62 | function Server(conf, engine){ 63 | EventEmitter.call(this); 64 | this.version = "0.9.36"; 65 | this.isDevelop = (conf.mode == 'test' || conf.mode == 'develop'); 66 | this.conf = conf; 67 | this.engine = engine; 68 | this.defaultEngine = defaultEngine; 69 | this.game = conf.game; 70 | this.wss = new SocketServer(conf); 71 | this.userlist = []; 72 | this.modes = conf.modes; 73 | this.init(); 74 | } 75 | 76 | Server.TIME_MODES = ['reset_every_turn', 'reset_every_switch', 'dont_reset', 'common']; 77 | Server.TIME_START_MODES = ['after_turn', 'after_switch', 'after_round_start']; 78 | 79 | util.inherits(Server, EventEmitter); 80 | 81 | Server.prototype.init = function(){ 82 | logger.log('GameServer.init', this.version, 'node: ', process.version, 0); 83 | 84 | var self = this; 85 | this.router = new Router(this); 86 | this.inviteManager = new InviteManager(this); 87 | this.ratingManager = new RatingManager(this); 88 | this.historyManager = new HistoryManager(this); 89 | this.gameManager = new GameManager(this); 90 | this.chatManager = new ChatManager(this); 91 | this.adminManager = new AdminManager(this); 92 | this.storage = this.isDevelop ? new (require('./develop/storage_interface.js'))(this) : new (require('./storage_interface.js'))(this); 93 | this.router.on('socket_disconnected', function(socket){ 94 | logger.log('Server', 'on socket_disconnected', socket.id, 2); 95 | var user = self.getUserById(socket.id); 96 | // if closeOldConnection old socket closed 97 | if (!user){ // TODO: do not log error on user relogin 98 | if (!socket.reconnectUser) 99 | logger.warn('Server','on socket_disconnected', 'user not found! socket: ', socket.id, socket.userId, 2); 100 | return; 101 | } 102 | user.isConnected = false; 103 | self.onUserLeave(user); 104 | }); 105 | 106 | this.router.on('socket_timeout', function(socket){ 107 | logger.log('Server', 'on socket_timeout', socket.id, 2); 108 | var user = self.getUserById(socket.id); 109 | if (!user){ 110 | logger.warn('Server', 'on socket_timeout', 'user not found! socket:', socket.id, socket.userId, 2); 111 | return; 112 | } 113 | user.isConnected = false; 114 | user.isTimeout = true; 115 | self.onUserLeave(user); 116 | }); 117 | }; 118 | 119 | 120 | Server.prototype.start = function(){ 121 | logger.log('Server', 'GameServer started, listening port:', this.conf.port, 1); 122 | this.wss.init(); 123 | }; 124 | 125 | 126 | Server.prototype.onUserLogin = function(user){ 127 | var self = this; 128 | // async get user data 129 | self.storage.getUserData(user) 130 | .then(function(data){ 131 | if (user.socket.closed || !user.socket.ws){ 132 | logger.warn('Server.onUserLogin socket closed', user.socket.id, user.socket.closed, !!user.socket.ws, 2); 133 | return; 134 | } 135 | user.applyData(data, self.modes); 136 | user.socket.enterRoom(self.game); 137 | self.sendUserLoginData(user); 138 | self.storage.pushUser(user); 139 | self.emit("user_login", user); 140 | }) 141 | .catch(function(error){ 142 | self.onLoginError(user, error); 143 | }); 144 | }; 145 | 146 | 147 | Server.prototype.onUserRelogin = function(newUser){ 148 | var user = this.storage.getUser(newUser.userId); 149 | if (!user) { 150 | logger.err("Server.onUserRelogin", "user not exists in userlist", newUser.userId, 1); 151 | return; 152 | } 153 | logger.log("Server.onUserRelogin", newUser.userId, 'old socket ', user.socket.id, 'new socket', newUser.socket.id, 2); 154 | 155 | // TODO: send message to closed socket throw error 156 | // TODO: check user is connected, but socket room does't exists in socketServer room list 157 | var oldSocket = user.socket; 158 | user.socket = newUser.socket; 159 | if (user.isConnected) { 160 | try { 161 | this.router.send({ 162 | module: 'server', 163 | type: 'error', 164 | target: oldSocket.id, 165 | data: 'new_connection' 166 | }); 167 | oldSocket.reconnectUser = true; 168 | oldSocket.close(); 169 | } catch (e){ 170 | logger.err("Server.onUserRelogin", "error on closing old socket", newUser.userId, oldSocket.id, e, 1); 171 | } 172 | } 173 | user.socket.enterRoom(this.game); 174 | this.storage.popUser(user); 175 | this.sendUserLoginData(user); 176 | this.storage.pushUser(user); 177 | user.isConnected = true; 178 | this.emit("user_relogin", user); 179 | }; 180 | 181 | 182 | Server.prototype.onLoginError = function(user, error){ 183 | logger.err('Server.onLoginError', 'user login failed;', error, 1); 184 | if (user.socket.closed || !user.socket.ws){ 185 | logger.warn('Server.onUserLogin socket closed', user.socket.id, user.socket.closed, !!user.socket.ws, 2); 186 | return; 187 | } 188 | this.router.send({ 189 | module:'server', 190 | type:'error', 191 | target:user, 192 | data:'login_error' 193 | }); 194 | }; 195 | 196 | 197 | Server.prototype.sendUserLoginData = function(user){ 198 | this.router.send({ 199 | module:'server', 200 | type:'login', 201 | target:user, 202 | data:{ 203 | you: user.getInfo(), 204 | userlist: this.getUserList(), 205 | rooms: this.storage.getRooms(), 206 | waiting: this.inviteManager.getWaitingUsers(), 207 | settings: user.settings, 208 | opts: { 209 | game: this.conf.game, 210 | modes: this.conf.modes, 211 | modesAlias: this.conf.modesAlias, 212 | turnTime: this.conf.turnTime, 213 | loadRanksInRating: this.conf.loadRanksInRating 214 | }, 215 | ban: user.ban 216 | } 217 | }); 218 | }; 219 | 220 | 221 | Server.prototype.onUserLeave = function(user){ 222 | var room = this.gameManager.getUserRoom(user, true); 223 | if (!room || !room.isPlaying()){ 224 | this.storage.popUser(user); 225 | } 226 | this.emit("user_leave", user); 227 | }; 228 | 229 | 230 | Server.prototype.onUserChanged = function(user, data){ 231 | logger.log('Server.onUserChanged, userId:',user.userId, 3); 232 | var now = Date.now(); 233 | if (data) { 234 | if (typeof data.isActive == 'boolean') user.isActive = data.isActive; 235 | } 236 | if (now - user.lastTimeSendInfo > 1000){ // send user info once a second 237 | this.sendUserInfo(user); 238 | user.lastTimeSendInfo = now; 239 | } else { 240 | logger.warn('Server.onUserChanged fast!', user.userId, 1) 241 | } 242 | }; 243 | 244 | Server.prototype.sendUserInfo = function(user){ 245 | this.router.send({ 246 | module:"server", 247 | type: "user_changed", 248 | sender:user, 249 | target:this.game, 250 | data:user.getInfo() 251 | }); 252 | }; 253 | 254 | 255 | Server.prototype.getUserById = function(id){ 256 | var userlist = this.storage.getUsers(); 257 | for (var i = 0; i < userlist.length; i++){ 258 | if (userlist[i].userId == id || userlist[i].socket.id == id){ 259 | return userlist[i]; 260 | } 261 | } 262 | logger.warn("Server.getUserById", "user not exists in userlist", id, 2); 263 | return null; 264 | }; 265 | 266 | 267 | Server.prototype.getUserList = function(){ 268 | var userlistInfo = [], userlist = this.storage.getUsers(); 269 | for (var i = 0; i < userlist.length; i++){ 270 | userlistInfo.push(userlist[i].getInfo()); 271 | } 272 | return userlistInfo; 273 | }; 274 | 275 | 276 | Server.prototype.initUserData = function(data){ 277 | data = data || {}; 278 | data.dateCreate = data.dateCreate || Date.now(); 279 | data.isAdmin = this.conf.adminList.indexOf(data.userId) != -1; 280 | data.options = null; 281 | var modeData, i; 282 | for (i = 0; i < this.modes.length; i++){ 283 | modeData = data[this.modes[i]]; 284 | if (!modeData) { 285 | modeData = {}; 286 | modeData['win'] = 0; 287 | modeData['lose'] = 0; 288 | modeData['draw'] = 0; 289 | modeData['games'] = 0; 290 | modeData['rank'] = 0; 291 | modeData['ratingElo'] = 1600; 292 | modeData['timeLastGame'] = 0; 293 | } 294 | if (typeof this.engine.initUserData == "function") 295 | modeData = this.engine.initUserData(this.modes[i], modeData); 296 | data[this.modes[i]] = modeData; 297 | } 298 | return data; 299 | }; 300 | 301 | 302 | Server.prototype.onMessage = function(message, type){ 303 | switch (type) { 304 | case 'login': 305 | this.loginSocket(message); 306 | break; 307 | case 'settings': // player ready to play 308 | if (message.data) 309 | this.storage.saveUserSettings(message.sender, message.data); 310 | break; 311 | case 'changed': 312 | this.onUserChanged(message.sender, message.data); 313 | break; 314 | } 315 | }; 316 | 317 | 318 | Server.prototype.loginSocket = function(message){ 319 | logger.log('Server.loginSocket id:', message.sender?message.sender.id:null, 'user:', message.data.userId, message.data.userName, 2); 320 | if (!message.sender || !message.sender.id){ 321 | logger.err('Server.loginSocket', 'wrong socket', 1); 322 | return; 323 | } 324 | var user = new User(message.sender, message.data); 325 | if (!user.userId || !user.userName || !user.sign){ 326 | this.onLoginError(user, 'wrong data'); 327 | return; 328 | } 329 | 330 | var signIsRight = false; // validate user data 331 | if (typeof this.engine.checkSign == "function") { 332 | signIsRight = this.engine.checkSign(user); 333 | } else { 334 | signIsRight = this.defaultEngine.checkSign(user); 335 | } 336 | if (!signIsRight){ 337 | logger.err('Server.loginSocket', 'User sign is wrong', user.userId, user.userName, user.sign, 1); 338 | this.onLoginError(user, 'wrong sign'); 339 | return; 340 | } 341 | 342 | user.isConnected = true; 343 | if (this.storage.getUser(user.userId) != null) { //check user already connected 344 | logger.log('Server.loginSocket', "user is already connected", user.userId, 2); 345 | if (this.conf.closeOldConnection) 346 | this.onUserRelogin(user); 347 | else { 348 | // TODO: send socket message already connected 349 | user.socket.close(); 350 | } 351 | return; 352 | } 353 | user.socket.userId = user.userId; 354 | this.onUserLogin(user); 355 | }; -------------------------------------------------------------------------------- /lib/storage_interface.js: -------------------------------------------------------------------------------- 1 | var Mongo = require('./mongo.js'); 2 | var util = require('util'); 3 | var redis = require("redis"); 4 | var logger = require('./logger.js'); 5 | 6 | module.exports = StorageInterface; 7 | 8 | function StorageInterface(server, callback){ 9 | this.users = []; 10 | this.allUsers = []; 11 | this.rooms = {}; 12 | this.roomsInfo = []; 13 | this.messages = []; 14 | this.lastMessages = []; 15 | this.games = []; 16 | this.history = []; 17 | this.LAST_MSG_COUNT = 10; 18 | this.server = server; 19 | this.mongo = new Mongo(server); 20 | this.redis = redis.createClient(server.conf.redis.port, server.conf.redis.host); 21 | this.redis.on("error", function (err) { 22 | logger.err('Redis ' + err, 1); 23 | }); 24 | 25 | // init mongo and then load ranks 26 | this.mongo.init(function(){ 27 | this.mongo.createIndexes() 28 | .then(function(){ 29 | logger.log('StorageInterface creating indexes complete', 1); 30 | var promises = []; 31 | for (var i = 0; i < server.modes.length; i++){ 32 | promises.push(this.loadRanks(server.modes[i])); 33 | } 34 | Promise.all(promises) 35 | .then(function () { 36 | logger.log('StorageInterface loading ranks complete', 2); 37 | if (callback) callback(); 38 | }) 39 | .catch(function (err) { 40 | logger.err('StorageInterface loading ranks failed', err, 0); 41 | process.exit(1); 42 | }); 43 | }.bind(this)) 44 | .catch(function(){ 45 | logger.err('StorageInterface creating indexes failed', 0); 46 | process.exit(1); 47 | }); 48 | }.bind(this)); 49 | } 50 | 51 | 52 | StorageInterface.prototype.getUserData = function(user){ 53 | logger.log('StorageInterface.getUserData', user.userId, 3); 54 | var self = this; 55 | return new Promise(function(res, rej){ 56 | if (!user.userId || user.userId == 'undefined'){ 57 | rej('can not login without user id! check cookies'); 58 | return; 59 | } 60 | self.mongo.getUserData(user.userId) 61 | .then(self.loadUserRating.bind(self)) 62 | .then(function(userData){ 63 | self.mongo.getUserSettings(userData.userId) 64 | .then(function (settings) { 65 | userData.settings = settings || {}; 66 | self.mongo.loadBan(userData.userId) 67 | .then(function (ban) { 68 | userData.ban = ban; 69 | res(userData); 70 | }) 71 | .catch(function (err) { // loading user ban failed 72 | rej('can not load user ban ' + userId); 73 | }) 74 | }); 75 | }) 76 | .catch(function(err){ // loading user data failed 77 | rej('can not load user data '+ err + ' userId: ' + user.userId); 78 | }); 79 | }); 80 | }; 81 | 82 | 83 | StorageInterface.prototype.saveUserSettings = function(user, settings){ 84 | user.settings = settings; 85 | this.mongo.saveUserSettings(user.userId, settings); 86 | }; 87 | 88 | 89 | //____________ User ___________ 90 | StorageInterface.prototype.pushUser = function(user){ 91 | this.users.push(user); 92 | }; 93 | 94 | 95 | StorageInterface.prototype.popUser = function(user){ 96 | var id = (user.userId ? user.userId : user); 97 | for (var i = 0; i < this.users.length; i++){ 98 | if (this.users[i].userId == id) { 99 | this.users.splice(i, 1); 100 | return true; 101 | } 102 | } 103 | logger.err("StorageInterface.popUser", "user not exists in userlist", user.userId, 2); 104 | return false; 105 | }; 106 | 107 | 108 | StorageInterface.prototype.getUser = function(id){ 109 | for (var i = 0; i < this.users.length; i++){ 110 | if (this.users[i].userId == id) { 111 | return this.users[i]; 112 | } 113 | } 114 | return null; 115 | }; 116 | 117 | 118 | StorageInterface.prototype.getUsers = function(){ 119 | return this.users; 120 | }; 121 | 122 | 123 | StorageInterface.prototype.saveUsers = function(users, mode, callback) { 124 | //TODO: refactor this 125 | var key = this.server.game + ':' + mode + ':' + 'ranks', self = this; 126 | 127 | for (var i = 0; i < users.length; i++) { 128 | var user = users[i]; 129 | this.redis.zadd(key, user[mode].ratingElo, user.userId); 130 | } 131 | 132 | saveUser(self.redis, users[0].getInfo(), mode, function(){ 133 | saveUser(self.redis, users[1].getInfo(), mode, callback); 134 | }); 135 | 136 | function saveUser(redis, data, mode, _callback){ 137 | self.redis.zrevrank(key, data.userId, function(err, result){ 138 | if (err || result === null) { 139 | logger.log('StorageInterface.saveUser get rank failed ', err||result, data.userId, 3); 140 | } else { 141 | data[mode].rank = result+1; 142 | logger.log('StorageInterface.saveUser redis rank', data.userId, result+1, 3); 143 | } 144 | self.mongo.saveUserData(data) 145 | .then(function () { 146 | logger.log('StorageInterface.saveUser success ', data.userId, mode, 3); 147 | }) 148 | .catch(function () { 149 | logger.err('StorageInterface.saveUser failed ', data.userId, mode, 2); 150 | }); 151 | _callback(); 152 | }); 153 | } 154 | }; 155 | 156 | 157 | //____________ Room ___________ 158 | StorageInterface.prototype.pushRoom = function(room){ 159 | this.rooms[room.id] = room; 160 | this.roomsInfo.push(room.getInfo()); 161 | }; 162 | 163 | 164 | StorageInterface.prototype.popRoom = function(room){ 165 | var id = (room.id ? room.id : room); 166 | delete this.rooms[id]; 167 | for (var i = 0; i < this.roomsInfo.length; i++){ 168 | if (this.roomsInfo[i].room == id) { 169 | this.roomsInfo.splice(i, 1); 170 | return true; 171 | } 172 | } 173 | logger.err("StorageInterface.popRoom", "room not exists in roomsInfo id:", id, 1); 174 | }; 175 | 176 | 177 | StorageInterface.prototype.getRoom = function(id){ 178 | return this.rooms[id]; 179 | }; 180 | 181 | 182 | StorageInterface.prototype.getRooms = function(){ 183 | return this.roomsInfo; 184 | }; 185 | 186 | 187 | //____________ Chat ___________ 188 | StorageInterface.prototype.pushMessage = function(message){ 189 | this.mongo.saveMessage(message); 190 | if (message.target == this.server.game) this.lastMessages.unshift(message); 191 | if (this.lastMessages.length>this.LAST_MSG_COUNT) this.lastMessages.pop(); 192 | }; 193 | 194 | StorageInterface.prototype.getMessages = function(count, time, target, sender){ 195 | var self = this; 196 | if (!sender && !time && self.lastMessages.length >= count) {// public messages, load from cache 197 | return new Promise(function(res){ 198 | res(self.lastMessages); 199 | }); 200 | } 201 | else return self.mongo.loadMessages(count, time?time:Date.now(), target, sender); 202 | }; 203 | 204 | StorageInterface.prototype.banUser = function(userId, timeEnd, reason){ 205 | this.mongo.saveBan({ 206 | userId:userId, 207 | timeEnd:timeEnd, 208 | timeStart: Date.now(), 209 | reason: reason 210 | }); 211 | }; 212 | 213 | StorageInterface.prototype.deleteMessage = function(id){ 214 | this.mongo.deleteMessage(id); 215 | for (var i = 0; i < this.lastMessages.length; i++){ 216 | if (this.lastMessages[i].time == id){ 217 | this.lastMessages.splice(i, 1); 218 | break; 219 | } 220 | } 221 | }; 222 | 223 | //____________ History ___________ 224 | StorageInterface.prototype.pushGame = function(save){ 225 | this.mongo.saveGame(save); 226 | }; 227 | 228 | 229 | StorageInterface.prototype.getGame = function(userId, gameId){ 230 | return this.mongo.loadGame(gameId); 231 | }; 232 | 233 | 234 | StorageInterface.prototype.getHistory = function(userId, mode, count, offset, filter){ 235 | count = +count; 236 | offset = +offset; 237 | if (!count || count < 0 || count > 1000) count = 50; 238 | if (!offset || offset < 0) offset = 0; 239 | if (typeof filter != "string" || (filter = filter.trim()).length < 1) { 240 | filter = false; 241 | } 242 | return Promise.all([ 243 | this.mongo.loadHistory(userId, mode, count, offset, filter), 244 | this.mongo.loadPenalties(userId, mode) 245 | ]); 246 | }; 247 | 248 | 249 | //_____________ Ratings ____________ 250 | StorageInterface.prototype.loadRanks = function(mode) { 251 | var self = this, count = 100000; var stTime= Date.now(); 252 | var key = self.server.game + ':' + mode + ':' + 'ranks'; 253 | self.redis.del(key); 254 | logger.log('StorageInterface.loadRanks', 'start loading users ranks, mode:', mode, 2); 255 | // recursive loading ranks 256 | return loadOrFinish(self.mongo.loadRanks(mode, count, 0)); 257 | 258 | function loadOrFinish(load){ 259 | return load.then(function (obj) { 260 | var item, items = obj.items, timeStart = Date.now(); 261 | for (var i = 0; i < items.length; i++){ 262 | item = items[i]; 263 | self.redis.zadd(key, item[mode].ratingElo, item.userId); 264 | } 265 | logger.log('StorageInterface.loadRanks', 'insert redis ranks ',items.length, 'time:', Date.now() - timeStart, 3); 266 | if (obj.items.length == count) { 267 | return loadOrFinish(self.mongo.loadRanks(mode, count, obj.skip + count)); 268 | } 269 | logger.log('StorageInterface.loadRanks', 'loading users ranks complete, mode:', mode, obj.skip+obj.items.length, 'ranks, time:', Date.now() - stTime, 3); 270 | return new Promise(function (res, rej) { 271 | res({ 272 | mode:mode, 273 | count:obj.skip + obj.items.length 274 | }); 275 | }); 276 | }); 277 | } 278 | }; 279 | 280 | 281 | StorageInterface.prototype.loadUserRating = function(userData){ 282 | var self = this; 283 | var promises = []; 284 | userData = self.server.initUserData(userData); 285 | for (var i = 0; i < self.server.modes.length; i++){ 286 | promises.push(this.loadUserRank(userData, self.server.modes[i])); 287 | } 288 | return new Promise(function (res, rej) { 289 | Promise.all(promises).then(function(){ 290 | res(userData); 291 | }).catch(function (err) { 292 | logger.err('StorageInterface.loadUserRating redis loadUserRating err', err, 1); 293 | res(userData); 294 | }); 295 | }); 296 | 297 | }; 298 | 299 | 300 | StorageInterface.prototype.loadUserRank = function (userData, mode){ 301 | return new Promise(function (res, rej) { 302 | var key = this.server.game + ':' + mode + ':' + 'ranks'; 303 | this.redis.zrevrank(key, userData.userId, function(err, result) { 304 | logger.log('StorageInterface.loadUserRank before', userData[mode].rank, 4); 305 | if (err || result === null) { 306 | if (err) logger.warn('StorageInterface.loadUserRating get rank failed ', err, userData.userId, 3); 307 | } else { 308 | userData[mode].rank = result + 1; 309 | } 310 | logger.log('StorageInterface.loadUserRank after', userData[mode].rank, 4); 311 | res(userData) 312 | }); 313 | }.bind(this)); 314 | }; 315 | 316 | 317 | StorageInterface.prototype.updateRatings = function(mode) { 318 | var promises = []; 319 | for (var i = 0; i < this.users.length; i++){ 320 | promises.push(this.loadUserRating(this.users[i])); 321 | } 322 | return Promise.all(promises); 323 | }; 324 | 325 | 326 | StorageInterface.prototype.getRatings = function(userId, params){ 327 | var count = +params.count; 328 | var offset = +params.offset; 329 | if (!count || count < 0 || count > 1000) count = 50; 330 | if (!offset || offset < 0) offset = 0; 331 | if (typeof params.filter != "string" || (params.filter = params.filter.trim()).length < 1) { 332 | params.filter = false; 333 | } 334 | return this.mongo.loadRating(params.mode, count, offset, params.column, params.order, params.filter) 335 | .then(function (allUsers) { 336 | if (!this.server.conf.loadRanksInRating) return allUsers; 337 | var promises = []; 338 | for (var i = 0; i < allUsers.length; i++) { 339 | promises.push(this.loadUserRank(allUsers[i], params.mode)); 340 | } 341 | return Promise.all(promises) 342 | }.bind(this)); 343 | }; -------------------------------------------------------------------------------- /lib/user.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var logger = require('./logger.js'); 3 | 4 | module.exports = User; 5 | 6 | function User(socket, userData){ 7 | this.userId = userData.userId ? userData.userId + '' : null; 8 | this.userName = userData.userName ? userData.userName + '' : null; 9 | this.sign = userData.sign ? userData.sign + '' : null; 10 | this.socket = socket; 11 | this.currentRoom = null; 12 | this.settings = {}; 13 | this.lastTimeSendInfo = Date.now(); 14 | this.isActive = true; 15 | } 16 | 17 | User.prototype.name = '__User__'; 18 | 19 | User.prototype.getInfo = function(mode){ 20 | var data = { 21 | userId: this.userId, 22 | userName: this.userName, 23 | dateCreate: this.dateCreate, 24 | disableInvite: this.settings.disableInvite || false, 25 | isActive: this.isActive 26 | }; 27 | if (mode){ 28 | data[mode] = this[mode]; 29 | } else { 30 | for (var i = 0; i < this.__modes.length; i++) 31 | data[this.__modes[i]] = this[this.__modes[i]]; 32 | } 33 | return data; 34 | }; 35 | 36 | User.prototype.getData = function(){ 37 | var data = {}; 38 | for (var i = 0; i < this.__modes.length; i++) 39 | data[this.__modes[i]] = this[this.__modes[i]]; 40 | return data; 41 | }; 42 | 43 | User.prototype.enterRoom = function(room){ 44 | if (this.currentRoom){ 45 | throw new Error('user ' + this.userId + ' already in room! ' + this.currentRoom.id); 46 | } 47 | this.socket.enterRoom(room.id); 48 | this.currentRoom = room; 49 | }; 50 | 51 | 52 | User.prototype.leaveRoom = function(){ 53 | if (!this.currentRoom) { 54 | logger.warn('no rooms to leave!', this.userId, 1); 55 | return; 56 | } 57 | if (this.isConnected) { // closed socket already leave room 58 | logger.log('leaving room ', this.currentRoom.id, this.userId, 3); 59 | // TODO socket leave already leaved room 60 | try { 61 | this.socket.leaveRoom(this.currentRoom.id); 62 | } catch(e){ 63 | logger.err('leaving room already leaved room', this.currentRoom.id, this.userId, e, 1); 64 | } 65 | } 66 | this.currentRoom = null; 67 | }; 68 | 69 | 70 | User.prototype.applyData = function(data, modes){ 71 | this.dateCreate = data.dateCreate; 72 | this.isBanned = data.isBanned; 73 | this.ban = data.ban; 74 | this.isAdmin = data.isAdmin || false; 75 | this.settings = data.settings; 76 | 77 | for (var i = 0; i < modes.length; i++) 78 | this[modes[i]] = data[modes[i]]; 79 | this.__modes = modes; 80 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "v6-game-server", 3 | "version": "0.9.36", 4 | "description": "V6 game server", 5 | "author": { 6 | "name": "King of Animals Lion" 7 | }, 8 | "main": "index.js", 9 | "dependencies": { 10 | "es6-promise": "*", 11 | "v6-ws": "0.3.13", 12 | "math": "*", 13 | "mongodb": "2.0.33", 14 | "redis":"*" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/levserk/v6-game-server" 19 | } 20 | } -------------------------------------------------------------------------------- /test/app.js: -------------------------------------------------------------------------------- 1 | var Server = require('../index.js'), 2 | engine = require('./engine.js'), 3 | conf = require('./conf.js'), 4 | server = new Server(conf, engine); 5 | 6 | server.start(); -------------------------------------------------------------------------------- /test/conf.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | game: 'test2', 3 | port: 8078, 4 | pingTimeout: 100000, 5 | pingInterval: 10000, 6 | logLevel: 3, 7 | turnTime: 60, 8 | maxTimeouts: 1, 9 | timeMode: 'reset_every_switch', 10 | timeStartMode: 'after_turn', 11 | addTime: 0, 12 | corTime: 5000, 13 | minTurns: 1, 14 | takeBacks: 1, 15 | loadRanksInRating: true, 16 | penalties: true, 17 | calcDraw: false, 18 | //mode: 'develop', 19 | gameModes: ['mode_1', 'mode_2'], 20 | modesAlias:{'mode_1':'mode first', 'mode_2': 'mode second'}, 21 | adminList: ['448039'], 22 | adminPass: '1', 23 | enableIpGames: false, 24 | minUnfocusedTurns: 1 25 | }; -------------------------------------------------------------------------------- /test/engine.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | initUserData: function(mode, modeData){ 3 | if (!modeData.score) modeData.score = 100; 4 | return modeData; 5 | }, 6 | initGame: function (room) { 7 | room.minTurns = 4; 8 | if (room.mode == 'mode_1'){ 9 | //room.timeMode = 'dont_reset'; 10 | room.turnTime = 15000; 11 | } 12 | return { 13 | inviteData: room.inviteData 14 | } 15 | }, 16 | getUsersScores: function(room, result){ 17 | for (var i = 0; i < room.players.length; i++){ 18 | if (room.players[i] == result.winner) 19 | room.players[i][room.mode].score += 10; 20 | else room.players[i][room.mode].score -= 10; 21 | } 22 | return result; 23 | }, 24 | switchPlayer:function(room, user, turn, type){ 25 | if (type=='timeout'){ 26 | console.log('switchPlayer', 'timeout', turn) 27 | } 28 | if (turn.switch || type == 'timeout'){ 29 | return room.getOpponent(user); 30 | } 31 | return user; 32 | }, 33 | doTurn: function(room, user, turn, type){ 34 | if (type=='timeout'){ 35 | console.log('doTurn', 'timeout', turn) 36 | } 37 | if (type == 'timeout' && room.data[user.userId].timeouts < room.maxTimeouts) { 38 | return turn; 39 | } 40 | if (turn.time){ 41 | room.setUserTurnTime(turn.time); 42 | } 43 | return turn; 44 | }, 45 | userEvent: function(room, user, event){ 46 | event.user = user.userId; 47 | return { 48 | event: event, 49 | target: room 50 | } 51 | }, 52 | gameEvent: function(room, user, event, flagRoundStart){ 53 | if (flagRoundStart){ 54 | var data = []; 55 | for (var i = 0; i < room.players.length; i++) { 56 | data.push({ 57 | target: room.players[i], 58 | event: { 59 | type: 'startEvent', 60 | data: room.players[i].userId 61 | } 62 | }); 63 | } 64 | return data; 65 | } 66 | }, 67 | getGameResult: function(room, user, turn, type){ 68 | switch (type){ 69 | case 'timeout': 70 | if (type == 'timeout'){ 71 | // if user have max timeouts, other win 72 | if (room.data[user.userId].timeouts == room.maxTimeouts){ 73 | return { 74 | winner: room.getOpponent(user), 75 | action: 'timeout' 76 | }; 77 | } else return false; 78 | } 79 | break; 80 | case 'event': 81 | if (turn.type == 'win'){ 82 | return { 83 | winner: user 84 | }; 85 | } else return false; 86 | break; 87 | case 'turn': 88 | switch (turn.result){ 89 | case 0: // win other player 90 | return { 91 | myRes: '1', 92 | winner: room.getOpponent(user) 93 | }; 94 | break; 95 | case 1: // win current player 96 | return { 97 | myRes: '1', 98 | winner: user 99 | }; 100 | break; 101 | case 2: // draw 102 | return { 103 | myRes: '1', 104 | winner: null 105 | }; 106 | break; 107 | default: // game isn't end 108 | return false; 109 | } 110 | break; 111 | } 112 | }, 113 | checkSign: function(user){ 114 | return (user.sign === user.userId + user.userName); 115 | }, 116 | adminAction: function(admin, type, data){ 117 | console.log(type); 118 | } 119 | }; --------------------------------------------------------------------------------