diff --git a/.gitignore b/.gitignore index 49401717f0..106f530652 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .sass-cache/ server/node_modules -logs/ \ No newline at end of file +logs/ +npm-debug.log +client/config.js diff --git a/README.md b/README.md index c6734433a5..69cd7fbfca 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,13 @@ -# 2048 -A small clone of [1024](https://play.google.com/store/apps/details?id=com.veewo.a1024), based on [Saming's 2048](saming.fr/p/2048/) (also a clone). +# 2048-multiplayer -Made just for fun. [Play it here!](http://gabrielecirulli.github.io/2048/) +A small multiplayer version of famous [2048](http://gabrielecirulli.github.io/2048/) -[![Screenshot](http://pictures.gabrielecirulli.com/2048-20140309-234100.png)](http://pictures.gabrielecirulli.com/2048-20140309-234100.png) +# Install -That screenshot is fake by, the way. I never reached 2048 :smile: +Create config file from dist if do not exist -## Contributing -Changes and improvements are more than welcome! Feel free to fork and open a pull request. Please make your changes in a specifically made branch and request to pull on `master`! If you can, please make sure the game fully works before sending the PR, as that will help speed up the process. +``` +$ cp client/config.js.dist client/config.js +``` -You can find the same information in the [contributing guide.](https://github.com/gabrielecirulli/2048/blob/master/CONTRIBUTING.md) - -## License -2048 is licensed under the [MIT license.](https://github.com/gabrielecirulli/2048/blob/master/LICENSE.txt) +Then edit `client/config.js` with your values. diff --git a/client/config.js.dist b/client/config.js.dist new file mode 100644 index 0000000000..a43de3d40f --- /dev/null +++ b/client/config.js.dist @@ -0,0 +1,4 @@ +window.Config = { + sockjsBaseUrl: 'http://localhost:3000', + defaultGameDuration: 120 // in seconds +}; diff --git a/client/index.html b/client/index.html index 2154d8f33c..60da591456 100644 --- a/client/index.html +++ b/client/index.html @@ -16,8 +16,9 @@

2048 - Multiplayer


Join the numbers and get to the 2048 tile! Your square is on the left!

@@ -25,20 +26,33 @@
Number of current games:
0
- -

- Find a Competitor - +

+ + + + + + + + + +

0
- +
- +

@@ -77,7 +91,7 @@
Number of current games:
- +

@@ -112,17 +126,17 @@
Number of current games:
-
+
- +

How to play: Use your arrow keys to move the tiles. When two tiles with the same number touch, they merge into one!


- Created by Gabriele Cirulli. Based on 1024 by Veewo Studio. Multiplayer added by Emil Stolarsky. + Created by Gabriele Cirulli. Based on 1024 by Veewo Studio. Multiplayer added by Emil Stolarsky.

@@ -132,6 +146,8 @@
Number of current games:
+ + diff --git a/client/js/application.js b/client/js/application.js index 83dcf0a944..1c92f9a3cd 100644 --- a/client/js/application.js +++ b/client/js/application.js @@ -1,225 +1,319 @@ document.addEventListener("DOMContentLoaded", function () { - var waitingInterval, userNumInterval; - var sockjs_url = 'http://2048.stolarsky.com:3000/game/sockets', sockjs, multiplexer; - - - - - // Wait till the browser is ready to render the game (avoids glitches) - window.requestAnimationFrame(function () { - - /* Dialog Box */ - vex.defaultOptions.className = 'vex-theme-default'; - - vex.dialog.open({ - message: 'Welcome to 2048 multiplayer! Use your 2048 skills to beat opponents!
\n
How it works: Good luck and may the squares be with you!', - contentCSS: { width: '750px' }, - buttons: [ - $.extend({}, vex.dialog.buttons.NO, { className: 'vex-dialog-button-primary', text: 'Give us a Tweet', click: function($vexContent, event) { - $vexContent.data().vex.value = 'tweet-btn'; - vex.close($vexContent.data().vex.id); - }}), - // $.extend({}, vex.dialog.buttons.NO, { className: 'vex-dialog-button-2048-friend', text: 'Play a Friend', click: function($vexContent, event) { - // $vexContent.data().vex.value = '2048-friend'; - // vex.close($vexContent.data().vex.id); - // }}), - $.extend({}, vex.dialog.buttons.NO, { className: 'vex-dialog-button-2048-simple game-start-btn', text: 'Find a Competitor', click: function($vexContent, event) { - $vexContent.data().vex.value = '2048-simple'; - vex.close($vexContent.data().vex.id); - }}) - ], - callback: function(value) { - if (value === 'tweet-btn') { - var tweetUrl = 'http://twitter.com/share?url=http%3A%2F%2Fbit.ly%2F1lFJnDg&text=Bet%20you%20can%27t%20beat%20me%20in%202048%20Multiplayer!&via=EmilStolarsky'; - window.open(tweetUrl, '_blank').focus(); - } - } - }); - - var startNewGame = function () { - // $('#player-msg').addClass('text-center'); - $('.game-start-btn').on('click', function () { - $('#player-msg').removeClass('text-center'); - $('#player-msg').html('Searching for competitor \n.\n.\n.'); - $('#game-stats').fadeIn(); - userNumInterval = setInterval(function () { - $.get('http://2048.stolarsky.com:3000/game/players', function (data) { - data = JSON.parse(data); - $('#num-players').html('Number of current players: ' + data.numPlayers); - $('#num-games').html('Number of current games: ' + data.numGames); - }); - }, 1000); - var fadedOut = false; - waitingInterval = setInterval(function () { - if (fadedOut) { - $('.ellipsis:eq(0)').fadeIn(500); - setTimeout(function() { - $('.ellipsis:eq(1)').fadeIn(500); - setTimeout(function() { - $('.ellipsis:eq(2)').fadeIn(500); - }, 250); - }, 250); - fadedOut = false; - } - else { - $('.ellipsis:eq(2)').fadeOut(500); - setTimeout(function() { - $('.ellipsis:eq(1)').fadeOut(500); - setTimeout(function() { - $('.ellipsis:eq(0)').fadeOut(500); - }, 250); - }, 250); - fadedOut = true; - } - }, 1500); - startGame(); - }); - }; - - var startGame = function () { - io = new SockJS(sockjs_url); - window._io = { - listeners: [], - oneTimeListeners: [], - addListener: function (cb) { - window._io.listeners.push(cb); - }, - addOneTimeListener: function (callback, onlyWhen) { - window._io.oneTimeListeners.push({ - cb: callback, - condition: onlyWhen - }); - }, - clearListeners: function () { - window._io.listeners = []; - window._io.oneTimeListeners = []; - } - } - - io.onopen = function() { - console.log('sockjs: open'); - }; - - io.onmessage = function(event) { - var msg = JSON.parse(event.data); - console.log('message:', msg); - for (var i = 0, len = window._io.listeners.length; i < len; i++) { - window._io.listeners[i](msg); - } - for (var i = window._io.oneTimeListeners.length - 1; i >= 0; i--) { - var tempObj = window._io.oneTimeListeners[i]; - if (!!tempObj.condition(msg)) { - tempObj.cb(msg); - window._io.oneTimeListeners.splice(i, 1); - } - } - }; - - /* Socket Listeners! */ - window._io.addListener(function (msg) { - if (msg.player && msg.size && msg.startCells) { - window._gameBoard = {}; - window._gameBoard.size = msg.size; - window._gameBoard.startTiles = msg.startCells; - window._gameBoard.player = msg.player; - } - }); - - window._io.addOneTimeListener(function (msg) { - clearInterval(waitingInterval); - clearInterval(userNumInterval); - $('#player-msg').addClass('text-center'); - $('#player-msg').html('Opponent Found!'); - $('#game-stats').fadeOut(); - setTimeout(function () { - window._io.player = {}; - window._io.player['1'] = 0; - window._io.player['2'] = 0; - window._io.gameOver = false; - var opposingPlayer = window._gameBoard.player === 1 ? 2 : 1; - var times = 3; - var countdown = setInterval(function() { - // Countdown messages - $('#player-msg').removeClass('text-center'); - $('#player-msg').html('
Game Will start in ' + times + '
'); - times--; - if (times === -1) { - clearInterval(countdown); - $('#player-msg').html('
BEGIN!
'); - var localManager = new GameManager({size: window._gameBoard.size, startTiles: window._gameBoard.startTiles, player: window._gameBoard.player, otherPlayer: opposingPlayer, online: false}, KeyboardInputManager, HTMLActuator, io), - onlineManager = new GameManager({size: window._gameBoard.size, startTiles: window._gameBoard.startTiles, player: opposingPlayer, otherPlayer: window._gameBoard.player, online: true}, OnlineInputManager, HTMLActuator, io); - - var gameOver = function (timer, message, connectionIssue) { - message = message || 'Game over!'; - clearInterval(timer); - $('#player-msg').html('
' + message + '
'); - window._io.gameOver = true; - /*if (connectionIssue) { - localManager.actuate(); - onlineManager.actuate(); - }*/ - localManager.actuate(); - onlineManager.actuate(); - setTimeout(function () { - $('#player-msg').fadeOut(); - }, 1000); - setTimeout(function () { - $('#player-msg').html(''); - $('#player-msg').fadeIn(); - }, 1500); - setTimeout(function () { - $('#player-msg').html('Find a Competitor!'); - startNewGame(); - window._io.clearListeners(); - io.close(); - $('.game-start-btn').on('click', function () { - localManager.restart(); - onlineManager.restart(); - }); - }, 3000); - }; - - var gameTimeLeft = 120;//game timer - var timer = setInterval(function () { - var sec; - if (gameTimeLeft % 60 === 0) - sec = '00'; - else if (('' + gameTimeLeft % 60).length === 1) - sec = '0' + gameTimeLeft % 60; - else - sec = gameTimeLeft % 60; - var min = Math.floor(gameTimeLeft/60); - $('#player-msg').html('
' + min + ':' + sec + '
'); - gameTimeLeft--; - if (gameTimeLeft === -1) { - gameOver(timer); - } - }, 750); - - window._io.addOneTimeListener(function (msg) { - gameOver(timer); - }, function (msg) { - return !!msg.gameEnd; - }); - - // window._io.addOneTimeListener(function (msg) { - // // console.log('msg sent was dead'); - // gameOver(timer, 'Connection Lost :('); - // }, function (msg) { - // return !!msg.dead; - // }); - } - }, 1000); - }, 1000); - }, function (msg) { - return !!msg.start; - }); - - io.onclose = function() { - console.log('sockjs: close'); - }; - }; - - startNewGame(); + var + userHash = window.hash(4), + userNumInterval = false, + registered = false, + friendHash = false, + friendGame = false; + + if (!window.Config) + throw new Error('config file must be present'); + + var sockjsUrl = window.Config.sockjsBaseUrl + '/game/sockets'; + + // Wait till the browser is ready to render the game (avoids glitches) + window.requestAnimationFrame(function () { + window.io = new SockJS(sockjsUrl); + + /* Dialog Box */ + vex.defaultOptions.className = 'vex-theme-default'; + vex.dialog.open({ + message: 'Welcome to 2048 multiplayer! Use your 2048 skills to beat opponents!
\n
How it works: Good luck and may the squares be with you!', + contentCSS: { width: '940px' }, + buttons: [ + $.extend({}, vex.dialog.buttons.NO, { className: 'vex-dialog-button-primary', text: 'Give us a Tweet', click: function ($vexContent, event) { + $vexContent.data().vex.value = 'tweet-btn'; + vex.close($vexContent.data().vex.id); + }}), + $.extend({}, vex.dialog.buttons.NO, { className: 'vex-dialog-button-2048-friend game-friend-btn', text: 'Play with a friend', click: function ($vexContent, event) { + $vexContent.data().vex.value = 'friend'; + vex.close($vexContent.data().vex.id); + }}), + $.extend({}, vex.dialog.buttons.NO, { className: 'vex-dialog-button-2048-simple game-start-btn', text: 'Find a random opponent', click: function ($vexContent, event) { + $vexContent.data().vex.value = 'random'; + vex.close($vexContent.data().vex.id); + }}) + ], + callback: function(value) { + if (value === 'random') { + $('.action-random').show(); + + return; + } + + if (value === 'tweet-btn') { + var tweetUrl = 'http://twitter.com/share?url=http%3A%2F%2Fbit.ly%2F1lFJnDg&text=Bet%20you%20can%27t%20beat%20me%20in%202048%20Multiplayer!&via=EmilStolarsky'; + window.open(tweetUrl, '_blank').focus(); + + return; + } + + vex.dialog.confirm({ + contentCSS: { width: '550px' }, + message: 'Please choose', + buttons: [ + $.extend({}, vex.dialog.buttons.YES, { + className: 'vex-dialog-button-primary', + text: 'Host game', + click: function ($vexContent) { + $vexContent.data().vex.value = 'host'; + vex.close($vexContent.data().vex.id); + } + }), + $.extend({}, vex.dialog.buttons.NO, { + className: 'vex-dialog-button-primary', + text: 'Join your friend', + click: function ($vexContent) { + $vexContent.data().vex.value = 'join'; + vex.close($vexContent.data().vex.id); + } + }) + ], + callback: function (value) { + friendGame = true; + + if ('join' === value) { + vex.dialog.prompt({ + message: 'Find your friend', + placeholder: 'Your friend unique hash here', + callback: function (value) { + friendHash = value; + startGame(value); + } + }); + + return; + } + + $('.action-wait-friend').show(); + startGame(null); + } + }); + } + }); + + var register = function () { + if (false !== registered) + return; + + window.io.send(JSON.stringify({ event: 'register', hash: userHash })); + + window._io = { + listeners: [], + oneTimeListeners: [], + addListener: function (cb) { + window._io.listeners.push(cb); + }, + addOneTimeListener: function (callback, onlyWhen) { + window._io.oneTimeListeners.push({ + cb: callback, + condition: onlyWhen + }); + }, + clearListeners: function () { + window._io.listeners = []; + window._io.oneTimeListeners = []; + } + }; + + window.io.onopen = function () { + console.log('sockjs: open'); + }; + + window.io.onmessage = function (event) { + var msg = JSON.parse(event.data); + console.log('message:', msg); + + if (msg.stats) { + return gameStats(msg); + } + + for (var i = 0, len = window._io.listeners.length; i < len; i++) { + window._io.listeners[i](msg); + } + + for (var i = window._io.oneTimeListeners.length - 1; i >= 0; i--) { + var tempObj = window._io.oneTimeListeners[i]; + + if (!!tempObj.condition(msg)) { + tempObj.cb(msg); + window._io.oneTimeListeners.splice(i, 1); + } + } + }; + + window.io.onclose = function () { + console.log('sockjs: close'); + }; + + registered = true; + }; + + var startGame = function (hash) { + register(); + + /* Socket Listeners! */ + window._io.addListener(function (msg) { + if (msg.player && msg.size && msg.startCells) { + window._gameBoard = {}; + window._gameBoard.size = msg.size; + window._gameBoard.startTiles = msg.startCells; + window._gameBoard.player = msg.player; + } + }); + + // wait for random opponent + if ('undefined' === typeof hash) { + window.io.send(JSON.stringify({ event: 'find-opponent' })); + + // find your friend with its hash + } else if (null !== hash) { + window.io.send(JSON.stringify({ event: 'play-friend', hash: hash })); + + // wait for your friend to find you and give its own hash + } else { + window._io.addOneTimeListener(function (msg) { + friendHash = msg.friendHash; + }, function (msg) { + return 'undefined' !== typeof msg.friendHash; + }); + } + + window._io.addOneTimeListener(function (msg) { + $('#player-msg .actions').fadeOut(); + $('#player-msg .live').fadeIn(); + $('#player-msg .live').html('
Opponent Found!
'); + + setTimeout(function () { + window._io.player = {}; + window._io.player['1'] = 0; + window._io.player['2'] = 0; + window._io.gameOver = false; + + var opposingPlayer = window._gameBoard.player === 1 ? 2 : 1; + var times = 3; + + var countdown = setInterval(function () { + // Countdown messages + $('#player-msg .live').html('
Game Will start in ' + times + '
'); + times--; + + if (times === -1) { + clearInterval(countdown); + clearInterval(userNumInterval); + userNumInterval = false; + + $('#player-msg .live').html('
BEGIN!
'); + + window.localManager = new GameManager({size: window._gameBoard.size, startTiles: window._gameBoard.startTiles, player: window._gameBoard.player, otherPlayer: opposingPlayer, online: false}, KeyboardInputManager, HTMLActuator, io); + window.onlineManager = new GameManager({size: window._gameBoard.size, startTiles: window._gameBoard.startTiles, player: opposingPlayer, otherPlayer: window._gameBoard.player, online: true}, OnlineInputManager, HTMLActuator, io); + + var gameTimeLeft = window.Config.defaultGameDuration; //game timer + + var timer = setInterval(function () { + var sec; + + if (gameTimeLeft % 60 === 0) + sec = '00'; + else if (('' + gameTimeLeft % 60).length === 1) + sec = '0' + gameTimeLeft % 60; + else + sec = gameTimeLeft % 60; + + var min = Math.floor(gameTimeLeft/60); + $('#player-msg .live').html('
' + min + ':' + sec + '
'); + gameTimeLeft--; + + if (gameTimeLeft === -1) { + gameOver(timer); + } + }, 750); + + window._io.addOneTimeListener(function () { + gameOver(timer); + }, function (msg) { + return !!msg.gameEnd; + }); + } + }, 1000); + }, 1000); + }, function (msg) { + return !!msg.start; + }); + }; + + var gameOver = function (timer, message) { + message = message || 'Game over!'; + clearInterval(timer); + + $('#player-msg .live').html('
' + message + '
'); + window._io.gameOver = true; + + window.localManager.actuate(); + window.onlineManager.actuate(); + + setTimeout(function () { + $('#player-msg .live').fadeOut(); + }, 1000); + + setTimeout(function () { + if (friendGame) { + $('.action-wait-friend').hide(); + + if (friendHash) { + $('.action-again-friend').show(); + } + } + + $('#player-msg .actions').fadeIn(); + }, 1500); + + setTimeout(function () { + window._io.clearListeners(); + + window._io.addOneTimeListener(function () { + window.localManager.restart(); + window.onlineManager.restart(); + }, function (msg) { + return !!msg.start || 'undefined' !== typeof msg.newFriendGame; + }); + + window._io.addOneTimeListener(function () { + window.localManager.restart(); + window.onlineManager.restart(); + startGame(null); + }, function (msg) { + return 'undefined' !== typeof msg.newFriendGame; + }); + }, 3000); + }; + + var gameStats = function (data) { + $('#game-stats').fadeIn(); + $('#num-players strong').text(data.numPlayers); + $('#num-waiters strong').text(data.numWaiters); + $('#num-games strong').text(data.numGames); + }; + + $('#your-hash strong').text(userHash); + + var startNewGame = function () { + $('.game-start-btn').on('click', function () { + $('#player-msg').removeClass('text-center'); + $('#player-msg .actions').fadeOut(); + $('#player-msg .live').fadeIn(); + $('#player-msg .live').html('
Searching for competitor...
'); + startGame(); + }); + + $('.action-again-friend a').on('click', function () { + window.io.send(JSON.stringify({ event: 'play-friend', hash: friendHash })); + window.localManager.restart(); + window.onlineManager.restart(); + startGame(null); + }); + }; + + startNewGame(); }); }); diff --git a/client/js/utils.js b/client/js/utils.js new file mode 100644 index 0000000000..21d49913c1 --- /dev/null +++ b/client/js/utils.js @@ -0,0 +1,15 @@ +window.hash = function(size) { + var chars = '123456789ABCDEFGHJKPQRSTUVWXYZabcdefghkpqrstuvwxyz', + len = chars.length, + hash = ''; + size = !isNaN(size) ? Math.max(size, 3) : 3; + for (var x = 0; x < size; x++) { + if (x === 0) { + // do not start with a number + hash += chars.charAt(Math.floor(Math.random() * (len - 10) + 10)); + } else { + hash += chars.charAt(Math.floor(Math.random() * len)); + } + } + return hash; +}; diff --git a/server/GameLobby.js b/server/GameLobby.js index 5fc113758b..0acde3b5d7 100644 --- a/server/GameLobby.js +++ b/server/GameLobby.js @@ -1,47 +1,54 @@ 'use strict'; - -/* +/* GameLobby constructs a new game lobby id - uuid of game loby - gamer1 - a SockJS connection instance of a gamer - gamer2 - a connection instance of a gamer + player1 - a SockJS connection instance of a player + player2 - a connection instance of a player */ -function GameLobby (id, gamer1, gamer2, startCells, size, cleanup) { +function GameLobby(id, player1, player2, startCells, size, cleanup) { this.id = id; - this.gamer1 = gamer1; - this.gamer2 = gamer2; + this.players = { + 1: player1, + 2: player2 + }; + this.startCells = startCells; this.size = size; this.cleanup = cleanup; - this.setup(gamer1, 1); - this.setup(gamer2, 2); + this.setup(1); + this.setup(2); } -GameLobby.prototype.setup = function(gamer, playerNum) { - var self = this; - gamer.write(JSON.stringify({player: playerNum, startCells: this.startCells, size: this.size, start: true})); - - gamer.on('data', function(data) { - self.emit(data); - }); - gamer.on('close', function() { - gamer.write(JSON.stringify({player: 0, dead: true})); - self.gamer1.close(); - self.gamer2.close(); - self.cleanup(self.id); - }); +GameLobby.prototype.setup = function (playerNum) { + var self = this; + + this.players[playerNum].write(JSON.stringify({ + player: playerNum, + startCells: this.startCells, + size: this.size, + start: true + })); + + this.players[playerNum].on('data', function (data) { + self.emit(data); + }); + + this.players[playerNum].on('close', function () { + self.emit(JSON.stringify({ player: playerNum, dead: true, gameEnd: true })); + self.cleanup(self.id); + }); }; -GameLobby.prototype.emit = function(msg) { - this.gamer1.write(msg); - this.gamer2.write(msg); - if (msg.gameEnd) { - this.gamer1.close(); - this.gamer2.close(); +GameLobby.prototype.emit = function (msg) { + this.players[1].write(msg); + this.players[2].write(msg); + + msg = JSON.parse(msg); + + if (msg.gameEnd) this.cleanup(this.id); - } }; -module.exports = GameLobby; \ No newline at end of file +module.exports = GameLobby; diff --git a/server/app.js b/server/app.js index 6b9b4d202c..7f11c1d4fb 100644 --- a/server/app.js +++ b/server/app.js @@ -10,106 +10,221 @@ var http = require('http'), client = redis.createClient(), gamersHashMap = {}, gamesBeingPlayed = 0, - gameStats = JSON.stringify({numPlayers: 0, numGames: 0}), - channelHashMap = {}, - channelId, - startLocations; - -var CROSS_ORIGIN_HEADERS = {}; -CROSS_ORIGIN_HEADERS['Content-Type'] = 'text/plain'; -CROSS_ORIGIN_HEADERS['Access-Control-Allow-Origin'] = '*'; -CROSS_ORIGIN_HEADERS['Access-Control-Allow-Headers'] = 'X-Requested-With'; + playersPublicHashMap = {}, + playersReversePublicHashMap = {}, + players = 0, + waiters = 0, + channelHashMap = {}; + +// ensure there is no garbage in Redis from previous sessions +client.del('players'); +client.del('waiters'); + +var CROSS_ORIGIN_HEADERS = { + 'Content-Type': 'text/plain', + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Headers': 'X-Requested-With' +}; + var sockjsServer = sockjs.createServer(); sockjsServer.setMaxListeners(0); var GRID_SIZE = 4; var cleanup = function (channelId) { - if (channelHashMap[channelId]) { - winston.info('===Game Cleanup==='); - winston.info('channelId:', channelId); - winston.info('channelHashMap[channelId].gamer1.id:', channelHashMap[channelId].gamer1.id); - winston.info('channelHashMap[channelId].gamer2.id:', channelHashMap[channelId].gamer2.id); - gamersHashMap[channelHashMap[channelId].gamer1.id] = void 0; - gamersHashMap[channelHashMap[channelId].gamer2.id] = void 0; - channelHashMap[channelId] = void 0; - gamesBeingPlayed--; - } + if (channelHashMap[channelId]) { + winston.info('===Game #' + channelId + ' Cleanup==='); + delete channelHashMap[channelId]; + gamesBeingPlayed--; + showStats(); + } }; -sockjsServer.on('connection', function(io) { - client.lpush('gamers', io.id); - io.on('close', function() { - client.lrem('gamers', 0, io.id, function (err, count) { - if (err) winston.log('err', err); - winston.info('Removed gamer from waiting queue'); - }); +sockjsServer.on('connection', function (io) { + client.lpush('players', io.id); + players++; + + winston.info('New player joined: ' + io.id); + showStats(); + + io.on('data', function (data) { + data = JSON.parse(data); + + if (!data.event) + return; + + switch (data.event) { + case 'register': + playersPublicHashMap[data.hash.toLowerCase()] = io; + playersReversePublicHashMap[io.id] = data.hash; + winston.info('New room registered: ' + data.hash); + break; + case 'find-opponent': + client.lpush('waiters', io.id); + waiters++; + findRandomOpponent(); + winston.info('New waiter is waiting (duh)'); + showStats(); + break; + case 'play-friend': + if (!data.hash || !data.hash.length) + return; + + if (!playersPublicHashMap[data.hash.toLowerCase()]) { + winston.info('Player ' + data.hash.toLowerCase() + ' not found.'); + return; + } + + winston.info('o/ found your friend, match is starting!'); + + playersPublicHashMap[data.hash.toLowerCase()].write(JSON.stringify({ + friendHash: playersReversePublicHashMap[io.id], + newFriendGame: true + })); + + startGame(io, playersPublicHashMap[data.hash.toLowerCase()]); + break; + case 'cleanup': + break; + default: + winston.info('Uncaught event `' + data.event + '` received'); + break; + } + }); + + io.on('close', function () { + client.lrem('players', 0, io.id, function (err) { + if (err) { + winston.log('err', err); + return; + } + + winston.info('Removed players from waiting queue'); + players--; + showStats(); + }); + + client.lrem('waiters', 0, io.id, function (err, count) { + if (err) { + winston.log('err', err); + return; + } + + if (count === 0) + return; + + winston.info('Removed waiter from waiting queue'); + waiters--; + showStats(); + }); }); + gamersHashMap[io.id] = io; }); var startCellLocations = function (numLocations, size) { var unique = function (arr, obj) { for (var i = 0, len = arr.length; i < len; i++) { - if (arr[i].x === obj.x && arr[i].y === obj.y) + if (arr[i].x === obj.x && arr[i].y === obj.y) return false; } return true; }; + var getRandomInt = function (min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; - } - + }; + var loc = []; for (var i = 0; i < numLocations; i++) { - var obj = {x: getRandomInt(0, size - 1), y: getRandomInt(0, size - 1), value: (Math.random() < 0.9 ? 2 : 4)}; - if (unique(loc, obj)) loc.push(obj); - else --i; + var obj = { + x: getRandomInt(0, size - 1), + y: getRandomInt(0, size - 1), + value: (Math.random() < 0.9 ? 2 : 4) + }; + + if (unique(loc, obj)) + loc.push(obj); + else + --i; } + return loc; }; -setInterval(function () { - client.llen('gamers', function (err, len) { +// todo: handle concurency ? +var findRandomOpponent = function () { + client.llen('waiters', function (err, len) { if (err) winston.log('err', err); if (len >= 2) { - client.lpop('gamers', function (err1, gamer1) { + client.lpop('waiters', function (err1, player1) { if (err1) winston.log('err', err1); - client.lpop('gamers', function (err2, gamer2) { + waiters--; + client.lpop('waiters', function (err2, player2) { if (err2) winston.log('err', err2); - winston.info('===New Game==='); - winston.info('channelId:', channelId); - winston.info('gamer1:', gamer1); - winston.info('gamer2:', gamer2); - channelId = uuid.v4(); - startLocations = startCellLocations (2, GRID_SIZE); - gamesBeingPlayed++; - channelHashMap[channelId] = new GameLobby (channelId, gamersHashMap[gamer1], gamersHashMap[gamer2], startLocations, GRID_SIZE, cleanup); + waiters--; + startGame(gamersHashMap[player1], gamersHashMap[player2]); }); }); } - }) -}, 500); + }); +}; + +var pushStats = function (stats, index) { + if ('undefined' === typeof index) { + client.llen('players', function (err, len) { + if (err) winston.log('err', err); + pushStats(stats, len - 1); + }); + return; + } + + if (-1 === index) { + return; + } -setInterval(function () { - client.llen('gamers', function(err, listSize) { + client.lindex('players', index, function (err, ioId) { if (err) winston.log('err', err); - winston.info('Number of current players: ' + (listSize + gamesBeingPlayed * 2)); - winston.info('Number of current games: ' + gamesBeingPlayed); - gameStats = JSON.stringify({numPlayers: (listSize + gamesBeingPlayed * 2), numGames: gamesBeingPlayed}); + winston.info('Broacasting stats to ' + ioId); + gamersHashMap[ioId].write(stats); + pushStats(stats, index - 1); }); -}, 1000); +}; + +var startGame = function (io1, io2) { + var id = uuid.v4(); + channelHashMap[id] = new GameLobby(id, io1, io2,startCellLocations(2, GRID_SIZE), GRID_SIZE, cleanup); + winston.info('=== New Game #' + id + ' started ==='); + + gamesBeingPlayed++; + showStats(); +}; var server = http.createServer(function (req, res) { if (url.parse(req.url).pathname === '/game/players') { res.writeHead(200, CROSS_ORIGIN_HEADERS); - res.write(gameStats); - res.end(); - } - else { - res.writeHead(200, {'Content-Type': 'text/html'}); - res.end('Go away <3'); + res.write(JSON.stringify({ + numPlayers: players, + numWaiters: waiters, + numGames: gamesBeingPlayed + })); + + return res.end(); } + + res.writeHead(200, {'Content-Type': 'text/html'}); + res.end('Go away <3'); }); -sockjsServer.installHandlers(server, {prefix:'/game/sockets'}); +var showStats = function () { + pushStats(JSON.stringify({ + stats: true, + numPlayers: players, + numWaiters: waiters, + numGames: gamesBeingPlayed + })); + + winston.info('Total players: ' + players + ' | Total waiters: ' + waiters + ' | Total games: ' + gamesBeingPlayed); +}; + +sockjsServer.installHandlers(server, { prefix: '/game/sockets' }); server.listen(3000); diff --git a/server/package.json b/server/package.json index 06ca3a3f3c..e74f276fc8 100644 --- a/server/package.json +++ b/server/package.json @@ -10,7 +10,8 @@ } ], "scripts": { - "start": "node app" + "start": "node app", + "start-prod": "supervisor app" }, "main": "./lib/http-server", "repository": { @@ -32,7 +33,9 @@ "node-uuid": "~1.4.1", "lodash": "~2.4.1", "engine.io": "~1.0.4", - "winston": "~0.7.2" + "winston": "~0.7.2", + "redis": "~0.10.1", + "supervisor": "*" }, "license": "MIT", "engines": {