'use strict'; process.title = 'mapguesser-multi'; class MultiGame { static ROUND_TIMEOUT_DEFAULT = 120000; static ROUND_TIMEOUT_MINIMUM = 15000; static ROUND_TIMEOUT_DIVIDER = 1.5; static ROUND_TIMEOUT_OFFSET = 500; constructor() { this.rooms = new Map(); } cleanupRooms() { this.rooms.forEach(function (room, roomId) { var lastValidDate = new Date(new Date().getTime() - 7 * 24 * 60 * 60 * 1000); if (room.updated < lastValidDate) { this.rooms.delete(roomId); } }); } connectToRoom(roomId, token, connection) { if (!this.rooms.has(roomId) || !this.rooms.get(roomId).members.has(token)) { return; } var room = this.rooms.get(roomId) var member = room.members.get(token); member.connection = connection; this._sendInitialData(room, member, token); } createRoom(roomId) { this.rooms.set(roomId, { members: new Map(), rounds: [], currentRound: -1, updated: new Date() }); return { ok: true }; } joinRoom(roomId, token, userName) { if (!this.rooms.has(roomId)) { return { error: 'room_not_found' }; } var room = this.rooms.get(roomId); room.updated = new Date(); if (room.members.has(token)) { return { error: 'member_already_joined' }; } var data = { userName: userName }; var self = this; room.members.forEach(function (member) { self._sendToMember(member, 'member_joined', data); }); room.members.set(token, { userName: userName, connection: null }); return { ok: true }; } startGame(roomId, places) { if (!this.rooms.has(roomId)) { return { error: 'room_not_found' }; } var room = this.rooms.get(roomId); room.updated = new Date(); var rounds = []; places.forEach(function (place) { rounds.push({ place: place, results: new Map(), timeout: MultiGame.ROUND_TIMEOUT_DEFAULT, timeoutStarted: null, timeoutHandler: null }) }); room.rounds = rounds; this.nextRound(roomId, 0); return { ok: true }; } guess(roomId, token, guessPosition, distance, score) { if (!this.rooms.has(roomId)) { return { error: 'room_not_found' }; } var room = this.rooms.get(roomId); room.updated = new Date(); var round = room.rounds[room.currentRound]; if (round.results.has(token)) { return { error: 'already_guessed' }; } var member = room.members.get(token); var allResults = this._collectResultsInRound(room, round); this._broadcastGuess(room, member.userName, guessPosition, distance, score); round.results.set(token, { guessPosition: guessPosition, distance: distance, score: score }); this._setNewTimeout(room, round); return { allResults: allResults }; } nextRound(roomId, currentRound) { if (!this.rooms.has(roomId)) { return { error: 'room_not_found' }; } var room = this.rooms.get(roomId); room.updated = new Date(); room.currentRound = currentRound; var round = room.rounds[room.currentRound]; round.timeoutStarted = new Date(); var self = this; round.timeoutHandler = setTimeout(function () { self._endRound(room, round); }, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET); var data = {}; data.place = { panoId: round.place.panoId, pov: round.place.pov }; data.timeout = round.timeout; var self = this; room.members.forEach(function (member) { self._sendToMember(member, 'new_round', data); }); return { ok: true }; } _setNewTimeout(room, round) { clearTimeout(round.timeoutHandler); if (room.members.size === round.results.size) { round.timeout = 0; round.timeoutStarted = new Date(); this._endRound(room, round); } else { round.timeout = round.timeout - (new Date() - round.timeoutStarted); if (round.timeout > MultiGame.ROUND_TIMEOUT_DIVIDER * MultiGame.ROUND_TIMEOUT_MINIMUM) { round.timeout = Math.round(round.timeout / MultiGame.ROUND_TIMEOUT_DIVIDER); } else if (round.timeout > MultiGame.ROUND_TIMEOUT_MINIMUM) { round.timeout = MultiGame.ROUND_TIMEOUT_MINIMUM; } round.timeoutStarted = new Date(); var self = this; round.timeoutHandler = setTimeout(function () { self._endRound(room, round); }, round.timeout + MultiGame.ROUND_TIMEOUT_OFFSET); this._broadcastTimeout(room, round); } } _endRound(room, round) { var allResults = this._collectResultsInRound(room, round); var self = this; room.members.forEach(function (member, token) { var result = { guessPosition: null, distance: null, score: 0 }; if (round.results.has(token)) { result = round.results.get(token); } else { round.results.set(token, result); } var data = { position: round.place.position, result: result, allResults: allResults }; self._sendToMember(member, 'end_round', data); }); } _sendInitialData(room, member, token) { var data = {}; if (room.currentRound >= 0) { var round = room.rounds[room.currentRound]; data.place = round.place; data.timeout = round.timeout - (new Date() - round.timeoutStarted); } data.history = []; for (var i = 0; i <= room.currentRound; ++i) { var round = room.rounds[i]; if (i === room.currentRound && !round.results.has(token)) { continue; } var result = { guessPosition: null, distance: null, score: 0 }; var allResults = []; round.results.forEach(function (currentResult, currentToken) { if (token === currentToken) { result = currentResult; return; } allResults.push({ userName: room.members.get(currentToken).userName, guessPosition: currentResult.guessPosition, distance: currentResult.distance, score: currentResult.score }); }); data.history.push({ position: round.place.position, result: result, allResults: allResults }); } data.members = []; room.members.forEach(function (currentMember) { data.members.push({ userName: currentMember.userName, me: member === currentMember }); }); data.readyToContinue = room.currentRound >= 0 && room.members.size === room.rounds[room.currentRound].results.size this._sendToMember(member, 'initialize', data); } _collectResultsInRound(room, round) { var results = []; round.results.forEach(function (result, token) { results.push({ userName: room.members.get(token).userName, guessPosition: result.guessPosition, distance: result.distance, score: result.score }); }); return results; } _broadcastTimeout(room, round) { var self = this; room.members.forEach(function (member) { self._sendToMember(member, 'timeout_changed', { timeout: round.timeout }); }); } _broadcastGuess(room, userName, guessPosition, distance, score) { var data = { userName: userName, guessPosition: guessPosition, distance: distance, score: score }; var round = room.rounds[room.currentRound]; var self = this; room.members.forEach(function (member, token) { if (!round.results.has(token)) { return; } self._sendToMember(member, 'guess', data); }); } _sendToMember(member, type, data) { if (!member.connection) { return; } if (member.connection.readyState !== ws.OPEN) { member.connection = null; return; } member.connection.send(JSON.stringify({ type: type, data: data })); } } require('dotenv').config(); var net = require('net'), ws = require('ws'); var multiGame = new MultiGame(); //TODO: following should be in a separate class/function var tcpServer = net.createServer(function (socket) { socket.on('data', function (data) { try { data = JSON.parse(data); } catch (e) { console.error('Cannot parse data: ' + data); return; } var response; switch (data.func) { case 'create_room': response = multiGame.createRoom(data.args.roomId); break; case 'join_room': response = multiGame.joinRoom(data.args.roomId, data.args.token, data.args.userName); break; case 'start_game': response = multiGame.startGame(data.args.roomId, data.args.places); break case 'guess': response = multiGame.guess(data.args.roomId, data.args.token, data.args.guessPosition, data.args.distance, data.args.score); break; case 'next_round': response = multiGame.nextRound(data.args.roomId, data.args.currentRound); break; } socket.write(JSON.stringify(response)); socket.end(); }); }); tcpServer.on('listening', function () { console.log('[INFO] TCP server started'); }); tcpServer.listen(process.env.MULTI_INTERNAL_PORT); var wsServer = new ws.Server({ port: process.env.MULTI_WS_PORT }); wsServer.on('connection', function (connection, request) { console.log('[INFO] New WS connection: ' + request.connection.remoteAddress); connection.on('message', function (data) { try { data = JSON.parse(data); } catch (e) { console.error('Cannot parse data: ' + data); return; } switch (data.func) { case 'connect_to_room': multiGame.connectToRoom(data.args.roomId, data.args.token, connection); break; } }); connection.on('close', function () { console.log('[INFO] WS connection ended: ' + request.connection.remoteAddress); }); }); wsServer.on('listening', function () { console.log('[INFO] WS server started'); }); setInterval(function () { multiGame.cleanupRooms(); }, 24 * 60 * 60 * 1000);