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
-
Number of current players:
- Number of current games:
+ Number of current players:
+ Number of current waiters:
+ Number of current games:
Join the numbers and get to the 2048 tile! Your square is on the left!
@@ -25,20 +26,33 @@ Number of current games:
0
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:- You only get 2 minutes
- If you can\'t make anymore legal moves, you lose
- Winner is decided by highest score or by who reaches 2048 first
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:- You only get 2 minutes
- If you can\'t make anymore legal moves, you lose
- Winner is decided by highest score or by who reaches 2048 first
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": {