├── Procfile ├── .gitignore ├── public ├── js │ ├── application.js │ ├── tile.js │ ├── grid.js │ ├── keyboard_input_manager.js │ ├── html_actuator.js │ ├── multiplayer.js │ ├── game_manager.js │ └── hammer.min.js └── style │ ├── fonts │ ├── ClearSans-Bold-webfont.eot │ ├── ClearSans-Bold-webfont.woff │ ├── ClearSans-Light-webfont.eot │ ├── ClearSans-Light-webfont.woff │ ├── ClearSans-Regular-webfont.eot │ ├── ClearSans-Regular-webfont.woff │ ├── clear-sans.css │ └── ClearSans-Bold-webfont.svg │ └── main.css ├── shell └── sass.sh ├── private ├── js │ ├── tile.js │ ├── game.js │ ├── grid.js │ └── game_manager.js └── sass │ ├── _helpers.sass │ └── main.sass ├── package.json ├── README.md ├── LICENSE ├── index.html └── app.js /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | .sass_cache 4 | **/.sass-cache/* 5 | -------------------------------------------------------------------------------- /public/js/application.js: -------------------------------------------------------------------------------- 1 | window.manager = new GameManager(4, KeyboardInputManager, HTMLActuator); -------------------------------------------------------------------------------- /public/style/fonts/ClearSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grant/hnplays2048/HEAD/public/style/fonts/ClearSans-Bold-webfont.eot -------------------------------------------------------------------------------- /public/style/fonts/ClearSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grant/hnplays2048/HEAD/public/style/fonts/ClearSans-Bold-webfont.woff -------------------------------------------------------------------------------- /public/style/fonts/ClearSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grant/hnplays2048/HEAD/public/style/fonts/ClearSans-Light-webfont.eot -------------------------------------------------------------------------------- /public/style/fonts/ClearSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grant/hnplays2048/HEAD/public/style/fonts/ClearSans-Light-webfont.woff -------------------------------------------------------------------------------- /public/style/fonts/ClearSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grant/hnplays2048/HEAD/public/style/fonts/ClearSans-Regular-webfont.eot -------------------------------------------------------------------------------- /public/style/fonts/ClearSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grant/hnplays2048/HEAD/public/style/fonts/ClearSans-Regular-webfont.woff -------------------------------------------------------------------------------- /shell/sass.sh: -------------------------------------------------------------------------------- 1 | echo 'Starting Sass' 2 | echo 'Ctrl-C to exit' 3 | echo '--------------' 4 | echo '' 5 | 6 | sass --watch ../private/sass/:../public/style/ --style compressed -------------------------------------------------------------------------------- /public/js/tile.js: -------------------------------------------------------------------------------- 1 | function Tile(position, value) { 2 | this.x = position.x; 3 | this.y = position.y; 4 | this.value = value || 2; 5 | 6 | this.previousPosition = null; 7 | this.mergedFrom = null; // Tracks tiles that merged together 8 | } 9 | 10 | Tile.prototype.savePosition = function () { 11 | this.previousPosition = { x: this.x, y: this.y }; 12 | }; 13 | 14 | Tile.prototype.updatePosition = function (position) { 15 | this.x = position.x; 16 | this.y = position.y; 17 | }; -------------------------------------------------------------------------------- /private/js/tile.js: -------------------------------------------------------------------------------- 1 | function Tile(position, value) { 2 | this.x = position.x; 3 | this.y = position.y; 4 | this.value = value || 2; 5 | 6 | this.previousPosition = null; 7 | this.mergedFrom = null; // Tracks tiles that merged together 8 | } 9 | 10 | Tile.prototype.savePosition = function () { 11 | this.previousPosition = { x: this.x, y: this.y }; 12 | }; 13 | 14 | Tile.prototype.updatePosition = function (position) { 15 | this.x = position.x; 16 | this.y = position.y; 17 | }; 18 | 19 | module.exports = Tile; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hnplays2048", 3 | "version": "0.0.0", 4 | "description": "Multiplayer 2048!", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/grant/hnplays2048.git" 12 | }, 13 | "keywords": [ 14 | "multiplayer", 15 | "2048" 16 | ], 17 | "author": "Grant Timmerman", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/grant/hnplays2048/issues" 21 | }, 22 | "homepage": "https://github.com/grant/hnplays2048", 23 | "dependencies": { 24 | "socket.io": "^0.9.16", 25 | "express": "^3.5.0" 26 | }, 27 | "engines": { 28 | "node": "0.10.x", 29 | "npm": "1.2.x" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | hnplays2048 2 | =========== 3 | 4 | A "Twitch Plays Pokemon" version of 2048. 5 | 6 | * Live version: http://hnplays2048.herokuapp.com 7 | * API: http://hnplays2048.herokuapp.com/api 8 | * Tweeted by Gabriele Cirulli: https://twitter.com/gabrielecirulli/statuses/444183831097397248 9 | * Featured on the top of HN on March 19, 2014. 10 | 11 | Screenshot 2025-02-01 at 19 58 24 12 | 13 | ## Setup 14 | 15 | Local: 16 | 17 | ``` 18 | git clone git@github.com:grant/hnplays2048.git 19 | npm install 20 | node app 21 | ``` 22 | 23 | Publishing: 24 | 25 | ``` 26 | git clone git@github.com:grant/hnplays2048.git 27 | npm install 28 | heroku create 29 | git push heroku master 30 | heroku open 31 | ``` 32 | 33 | Developing: 34 | 35 | ``` 36 | node app 37 | shell shell/sass.sh 38 | ``` 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Grant Timmerman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /public/style/fonts/clear-sans.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Clear Sans"; 3 | src: url("ClearSans-Light-webfont.eot"); 4 | src: url("ClearSans-Light-webfont.eot?#iefix") format("embedded-opentype"), 5 | url("ClearSans-Light-webfont.svg#clear_sans_lightregular") format("svg"), 6 | url("ClearSans-Light-webfont.woff") format("woff"); 7 | font-weight: 200; 8 | font-style: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: "Clear Sans"; 13 | src: url("ClearSans-Regular-webfont.eot"); 14 | src: url("ClearSans-Regular-webfont.eot?#iefix") format("embedded-opentype"), 15 | url("ClearSans-Regular-webfont.svg#clear_sansregular") format("svg"), 16 | url("ClearSans-Regular-webfont.woff") format("woff"); 17 | font-weight: normal; 18 | font-style: normal; 19 | } 20 | 21 | @font-face { 22 | font-family: "Clear Sans"; 23 | src: url("ClearSans-Bold-webfont.eot"); 24 | src: url("ClearSans-Bold-webfont.eot?#iefix") format("embedded-opentype"), 25 | url("ClearSans-Bold-webfont.svg#clear_sansbold") format("svg"), 26 | url("ClearSans-Bold-webfont.woff") format("woff"); 27 | font-weight: 700; 28 | font-style: normal; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /private/sass/_helpers.sass: -------------------------------------------------------------------------------- 1 | // Exponent 2 | // From: https://github.com/Team-Sass/Sassy-math/blob/master/sass/math.scss#L36 3 | 4 | @function exponent($base, $exponent) 5 | // reset value 6 | $value: $base 7 | // positive intergers get multiplied 8 | @if $exponent > 1 9 | @for $i from 2 through $exponent 10 | $value: $value * $base 11 | // negitive intergers get divided. A number divided by itself is 1 12 | @if $exponent < 1 13 | @for $i from 0 through -$exponent 14 | $value: $value / $base 15 | // return the last value written 16 | @return $value 17 | 18 | @function pow($base, $exponent) 19 | @return exponent($base, $exponent) 20 | 21 | // Transition mixins 22 | =transition($args...) 23 | -webkit-transition: $args 24 | -moz-transition: $args 25 | 26 | =transition-property($args...) 27 | -webkit-transition-property: $args 28 | -moz-transition-property: $args 29 | 30 | // Keyframe animations 31 | =keyframes($animation-name) 32 | @-webkit-keyframes #{$animation-name} 33 | @content 34 | 35 | @-moz-keyframes #{$animation-name} 36 | @content 37 | 38 | @keyframes #{$animation-name} 39 | @content 40 | 41 | =animation($str) 42 | -webkit-animation: #{$str} 43 | -moz-animation: #{$str} 44 | 45 | =animation-fill-mode($str) 46 | -webkit-animation-fill-mode: #{$str} 47 | -moz-animation-fill-mode: #{$str} 48 | 49 | // Media queries 50 | =smaller($width) 51 | @media screen and (max-width: $width) 52 | @content -------------------------------------------------------------------------------- /private/js/game.js: -------------------------------------------------------------------------------- 1 | // The server-side game 2 | 3 | // Requires 4 | var GameManager = require('./game_manager'); 5 | 6 | var gameManager = new GameManager(4); 7 | var scores = []; // Array of objects which include {date, score}. In sorted descending order 8 | var isRestarting = false; 9 | 10 | // External API 11 | module.exports = { 12 | 13 | // Game move 14 | move: function (direction) { 15 | gameManager.move(direction); 16 | }, 17 | 18 | // Gets the game state data 19 | getGameData: function () { 20 | return gameManager.getGameData(); 21 | }, 22 | 23 | // Resets the game 24 | restart: function (callback) { 25 | // Add score once 26 | var gameData = gameManager.getGameData(); 27 | var score = gameData.score; 28 | var won = gameData.won; 29 | if (!isRestarting) { 30 | isRestarting = true; 31 | addScore(score, won); 32 | // Restart the game after a short duration 33 | setTimeout(function () { 34 | isRestarting = false; 35 | gameManager.restart(); 36 | callback(); 37 | }, 4000); 38 | } 39 | }, 40 | 41 | // Gets all scores (could be a lot) 42 | getScores: function () { 43 | return scores; 44 | }, 45 | 46 | // Gets the top few scores 47 | getHighscores: function () { 48 | return scores.slice(0, 20); 49 | } 50 | }; 51 | 52 | // Add a score to the high score list 53 | function addScore (score, won) { 54 | scores.push({ 55 | date: new Date(), 56 | score: score, 57 | won: won 58 | }); 59 | 60 | // Keep scores sorted 61 | scores.sort(function (a, b) { 62 | return b.score - a.score; 63 | }); 64 | } -------------------------------------------------------------------------------- /public/js/grid.js: -------------------------------------------------------------------------------- 1 | function Grid(size) { 2 | this.size = size; 3 | 4 | this.cells = []; 5 | 6 | this.build(); 7 | } 8 | 9 | // Build a grid of the specified size 10 | Grid.prototype.build = function () { 11 | for (var x = 0; x < this.size; x++) { 12 | var row = this.cells[x] = []; 13 | 14 | for (var y = 0; y < this.size; y++) { 15 | row.push(null); 16 | } 17 | } 18 | }; 19 | 20 | // Find the first available random position 21 | Grid.prototype.randomAvailableCell = function () { 22 | var cells = this.availableCells(); 23 | 24 | if (cells.length) { 25 | return cells[Math.floor(Math.random() * cells.length)]; 26 | } 27 | }; 28 | 29 | Grid.prototype.availableCells = function () { 30 | var cells = []; 31 | 32 | this.eachCell(function (x, y, tile) { 33 | if (!tile) { 34 | cells.push({ x: x, y: y }); 35 | } 36 | }); 37 | 38 | return cells; 39 | }; 40 | 41 | // Call callback for every cell 42 | Grid.prototype.eachCell = function (callback) { 43 | for (var x = 0; x < this.size; x++) { 44 | for (var y = 0; y < this.size; y++) { 45 | callback(x, y, this.cells[x][y]); 46 | } 47 | } 48 | }; 49 | 50 | // Check if there are any cells available 51 | Grid.prototype.cellsAvailable = function () { 52 | return !!this.availableCells().length; 53 | }; 54 | 55 | // Check if the specified cell is taken 56 | Grid.prototype.cellAvailable = function (cell) { 57 | return !this.cellOccupied(cell); 58 | }; 59 | 60 | Grid.prototype.cellOccupied = function (cell) { 61 | return !!this.cellContent(cell); 62 | }; 63 | 64 | Grid.prototype.cellContent = function (cell) { 65 | if (this.withinBounds(cell)) { 66 | return this.cells[cell.x][cell.y]; 67 | } else { 68 | return null; 69 | } 70 | }; 71 | 72 | // Inserts a tile at its position 73 | Grid.prototype.insertTile = function (tile) { 74 | this.cells[tile.x][tile.y] = tile; 75 | }; 76 | 77 | Grid.prototype.removeTile = function (tile) { 78 | this.cells[tile.x][tile.y] = null; 79 | }; 80 | 81 | Grid.prototype.withinBounds = function (position) { 82 | return position.x >= 0 && position.x < this.size && 83 | position.y >= 0 && position.y < this.size; 84 | }; 85 | -------------------------------------------------------------------------------- /private/js/grid.js: -------------------------------------------------------------------------------- 1 | function Grid(size) { 2 | this.size = size; 3 | 4 | this.cells = []; 5 | 6 | this.build(); 7 | } 8 | 9 | // Build a grid of the specified size 10 | Grid.prototype.build = function () { 11 | for (var x = 0; x < this.size; x++) { 12 | var row = this.cells[x] = []; 13 | 14 | for (var y = 0; y < this.size; y++) { 15 | row.push(null); 16 | } 17 | } 18 | }; 19 | 20 | // Find the first available random position 21 | Grid.prototype.randomAvailableCell = function () { 22 | var cells = this.availableCells(); 23 | 24 | if (cells.length) { 25 | return cells[Math.floor(Math.random() * cells.length)]; 26 | } 27 | }; 28 | 29 | Grid.prototype.availableCells = function () { 30 | var cells = []; 31 | 32 | this.eachCell(function (x, y, tile) { 33 | if (!tile) { 34 | cells.push({ x: x, y: y }); 35 | } 36 | }); 37 | 38 | return cells; 39 | }; 40 | 41 | // Call callback for every cell 42 | Grid.prototype.eachCell = function (callback) { 43 | for (var x = 0; x < this.size; x++) { 44 | for (var y = 0; y < this.size; y++) { 45 | callback(x, y, this.cells[x][y]); 46 | } 47 | } 48 | }; 49 | 50 | // Check if there are any cells available 51 | Grid.prototype.cellsAvailable = function () { 52 | return !!this.availableCells().length; 53 | }; 54 | 55 | // Check if the specified cell is taken 56 | Grid.prototype.cellAvailable = function (cell) { 57 | return !this.cellOccupied(cell); 58 | }; 59 | 60 | Grid.prototype.cellOccupied = function (cell) { 61 | return !!this.cellContent(cell); 62 | }; 63 | 64 | Grid.prototype.cellContent = function (cell) { 65 | if (this.withinBounds(cell)) { 66 | return this.cells[cell.x][cell.y]; 67 | } else { 68 | return null; 69 | } 70 | }; 71 | 72 | // Inserts a tile at its position 73 | Grid.prototype.insertTile = function (tile) { 74 | this.cells[tile.x][tile.y] = tile; 75 | }; 76 | 77 | Grid.prototype.removeTile = function (tile) { 78 | this.cells[tile.x][tile.y] = null; 79 | }; 80 | 81 | Grid.prototype.withinBounds = function (position) { 82 | return position.x >= 0 && position.x < this.size && 83 | position.y >= 0 && position.y < this.size; 84 | }; 85 | 86 | module.exports = Grid; -------------------------------------------------------------------------------- /public/js/keyboard_input_manager.js: -------------------------------------------------------------------------------- 1 | function KeyboardInputManager() { 2 | this.events = {}; 3 | 4 | this.listen(); 5 | } 6 | 7 | KeyboardInputManager.prototype.on = function (event, callback) { 8 | if (!this.events[event]) { 9 | this.events[event] = []; 10 | } 11 | this.events[event].push(callback); 12 | }; 13 | 14 | // Keep track of the past events (queue) 15 | var numMovesPerSecond = 2; 16 | var pastEvents = []; 17 | for (var i = 0; i < numMovesPerSecond; i++) { 18 | pastEvents.push(0); 19 | } 20 | KeyboardInputManager.prototype.emit = function (event, data) { 21 | var callbacks = this.events[event]; 22 | 23 | // Keep track of events 24 | pastEvents.push(new Date().getTime()); 25 | pastEvents.splice(0, pastEvents.length - numMovesPerSecond); 26 | 27 | // Multiplayer 28 | var spamming = pastEvents[pastEvents.length - 1] - pastEvents[0] < 1000; 29 | if (Multiplayer[event] && !spamming) { 30 | Multiplayer[event](data); 31 | } 32 | }; 33 | 34 | KeyboardInputManager.prototype.listen = function () { 35 | var self = this; 36 | 37 | var map = { 38 | 38: 0, // Up 39 | 39: 1, // Right 40 | 40: 2, // Down 41 | 37: 3, // Left 42 | 75: 0, // vim keybindings 43 | 76: 1, 44 | 74: 2, 45 | 72: 3 46 | }; 47 | 48 | document.addEventListener("keydown", function (event) { 49 | var modifiers = event.altKey || event.ctrlKey || event.metaKey || 50 | event.shiftKey; 51 | var mapped = map[event.which]; 52 | 53 | if (!modifiers) { 54 | if (mapped !== undefined) { 55 | event.preventDefault(); 56 | self.emit("move", mapped); 57 | } 58 | 59 | if (event.which === 32) self.restart.bind(self)(event); 60 | } 61 | }); 62 | 63 | var retry = document.getElementsByClassName("retry-button")[0]; 64 | retry.addEventListener("click", this.restart.bind(this)); 65 | 66 | // Listen to swipe events 67 | var gestures = [Hammer.DIRECTION_UP, Hammer.DIRECTION_RIGHT, 68 | Hammer.DIRECTION_DOWN, Hammer.DIRECTION_LEFT]; 69 | 70 | var gameContainer = document.getElementsByClassName("game-container")[0]; 71 | var handler = Hammer(gameContainer, { 72 | drag_block_horizontal: true, 73 | drag_block_vertical: true 74 | }); 75 | 76 | handler.on("swipe", function (event) { 77 | event.gesture.preventDefault(); 78 | mapped = gestures.indexOf(event.gesture.direction); 79 | 80 | if (mapped !== -1) self.emit("move", mapped); 81 | }); 82 | }; 83 | 84 | KeyboardInputManager.prototype.restart = function (event) { 85 | event.preventDefault(); 86 | this.emit("restart"); 87 | }; -------------------------------------------------------------------------------- /public/js/html_actuator.js: -------------------------------------------------------------------------------- 1 | function HTMLActuator() { 2 | this.tileContainer = document.getElementsByClassName("tile-container")[0]; 3 | this.scoreContainer = document.getElementsByClassName("score-container")[0]; 4 | this.messageContainer = document.getElementsByClassName("game-message")[0]; 5 | 6 | this.score = 0; 7 | } 8 | 9 | HTMLActuator.prototype.actuate = function (grid, metadata) { 10 | var self = this; 11 | 12 | window.requestAnimationFrame(function () { 13 | self.clearContainer(self.tileContainer); 14 | 15 | grid.cells.forEach(function (column) { 16 | column.forEach(function (cell) { 17 | if (cell) { 18 | self.addTile(cell); 19 | } 20 | }); 21 | }); 22 | 23 | self.updateScore(metadata.score); 24 | 25 | if (metadata.over) self.message(false); // You lose 26 | if (metadata.won) self.message(true); // You win! 27 | 28 | // HACK: Fixes glitch where the game over screen would not go away even though the game was not over 29 | if (self.messageContainer.classList.contains('game-over')) { 30 | setTimeout(function() { 31 | self.clearMessage(); 32 | }, 5000); 33 | } 34 | }); 35 | }; 36 | 37 | HTMLActuator.prototype.restart = function () { 38 | this.clearMessage(); 39 | }; 40 | 41 | HTMLActuator.prototype.clearContainer = function (container) { 42 | while (container.firstChild) { 43 | container.removeChild(container.firstChild); 44 | } 45 | }; 46 | 47 | HTMLActuator.prototype.addTile = function (tile) { 48 | var self = this; 49 | 50 | var element = document.createElement("div"); 51 | var position = tile.previousPosition || { x: tile.x, y: tile.y }; 52 | positionClass = this.positionClass(position); 53 | 54 | // We can't use classlist because it somehow glitches when replacing classes 55 | var classes = ["tile", "tile-" + tile.value, positionClass]; 56 | this.applyClasses(element, classes); 57 | 58 | element.textContent = tile.value; 59 | 60 | if (tile.previousPosition) { 61 | // Make sure that the tile gets rendered in the previous position first 62 | window.requestAnimationFrame(function () { 63 | classes[2] = self.positionClass({ x: tile.x, y: tile.y }); 64 | self.applyClasses(element, classes); // Update the position 65 | }); 66 | } else if (tile.mergedFrom) { 67 | classes.push("tile-merged"); 68 | this.applyClasses(element, classes); 69 | 70 | // Render the tiles that merged 71 | tile.mergedFrom.forEach(function (merged) { 72 | self.addTile(merged); 73 | }); 74 | } else { 75 | classes.push("tile-new"); 76 | this.applyClasses(element, classes); 77 | } 78 | 79 | // Put the tile on the board 80 | this.tileContainer.appendChild(element); 81 | }; 82 | 83 | HTMLActuator.prototype.applyClasses = function (element, classes) { 84 | element.setAttribute("class", classes.join(" ")); 85 | }; 86 | 87 | HTMLActuator.prototype.normalizePosition = function (position) { 88 | return { x: position.x + 1, y: position.y + 1 }; 89 | }; 90 | 91 | HTMLActuator.prototype.positionClass = function (position) { 92 | position = this.normalizePosition(position); 93 | return "tile-position-" + position.x + "-" + position.y; 94 | }; 95 | 96 | HTMLActuator.prototype.updateScore = function (score) { 97 | this.clearContainer(this.scoreContainer); 98 | 99 | var difference = score - this.score; 100 | this.score = score; 101 | 102 | this.scoreContainer.textContent = this.score; 103 | 104 | if (difference > 0) { 105 | var addition = document.createElement("div"); 106 | addition.classList.add("score-addition"); 107 | addition.textContent = "+" + difference; 108 | 109 | this.scoreContainer.appendChild(addition); 110 | } 111 | }; 112 | 113 | HTMLActuator.prototype.message = function (won) { 114 | var type = won ? "game-won" : "game-over"; 115 | var message = won ? "You win!" : "Game over!"; 116 | 117 | // if (ga) ga("send", "event", "game", "end", type, this.score); 118 | 119 | this.messageContainer.classList.add(type); 120 | this.messageContainer.getElementsByTagName("p")[0].textContent = message; 121 | }; 122 | 123 | HTMLActuator.prototype.clearMessage = function () { 124 | this.messageContainer.classList.remove("game-won", "game-over"); 125 | }; -------------------------------------------------------------------------------- /public/js/multiplayer.js: -------------------------------------------------------------------------------- 1 | var moveList = document.getElementsByClassName("inputlist")[0]; 2 | var highscoreList = document.getElementsByClassName("scorelist")[0]; 3 | var userCount = document.getElementsByClassName("numUsers")[0]; 4 | var userString = document.getElementsByClassName("userString")[0]; 5 | var arrows = ['▲', '▶', '▼', '◀']; 6 | var MOVE_LIST_CUTOFF = 20; 7 | 8 | var democracy = true; 9 | 10 | var socket = io.connect(); 11 | var yourUserId; 12 | socket.on('connected', function (data) { 13 | yourUserId = data.userId; 14 | var gameData = data.gameData; 15 | var highscores = data.highscores; 16 | setHighscores(highscores); 17 | updateUserCount(data); 18 | manager.setGameData(gameData); 19 | }); 20 | 21 | socket.on('someone connected', function (data) { 22 | updateUserCount(data); 23 | }); 24 | 25 | socket.on('someone disconnected', function (data) { 26 | updateUserCount(data); 27 | }); 28 | 29 | socket.on('move', function (data) { 30 | // Add move to input list 31 | var direction = data.direction; 32 | var userId = data.userId; 33 | var moveElement = document.createElement('li'); 34 | 35 | var userIdString = 'User ' + userId; 36 | if (userId === yourUserId) { 37 | userIdString = '' + userIdString + ''; 38 | } 39 | moveElement.innerHTML = '' + arrows[direction] + '' + userIdString; 40 | moveList.insertBefore(moveElement,moveList.firstChild); 41 | 42 | // Remove input list item if there are too many 43 | var moveListLen = moveList.childNodes.length; 44 | if (moveListLen > MOVE_LIST_CUTOFF) { 45 | for (var i = MOVE_LIST_CUTOFF; i < moveListLen; ++i) { 46 | moveList.removeChild(moveList.childNodes[i]); 47 | } 48 | } 49 | 50 | // Update number of users 51 | updateUserCount(data); 52 | 53 | // Set the game state (if we're not in a pause state) 54 | if (!(manager.won || manager.over)) { 55 | if (!democracy || userId == "Democracy") { 56 | var gameData = data.gameData; 57 | manager.setGameData(gameData); 58 | } 59 | } 60 | }); 61 | 62 | socket.on('restart', function (gameData) { 63 | var highscores = gameData.highscores; 64 | setHighscores(highscores); 65 | manager.restart(); 66 | manager.setGameData(gameData); 67 | }); 68 | 69 | // Sets the visual high score list 70 | function setHighscores (highscores) { 71 | // Remove all scores 72 | highscoreList.innerHTML = ''; 73 | 74 | // Add all scores 75 | for (var i = 0; i < highscores.length; ++i) { 76 | var hsElement = document.createElement('li'); 77 | var hs = highscores[i]; 78 | var hsString = '' + hs.score + '' + prettyDate(new Date(hs.date)); 79 | if (hs.won) { 80 | hsElement.innerHTML = '' + hsString + ''; 81 | } else { 82 | hsElement.innerHTML = hsString; 83 | } 84 | highscoreList.appendChild(hsElement); 85 | } 86 | } 87 | 88 | // Update the user count 89 | function updateUserCount (data) { 90 | var numUsers = data.numUsers; 91 | userCount.innerHTML = numUsers; 92 | userString.innerHTML = numUsers === 1 ? 'user' : 'users'; 93 | } 94 | 95 | //// Pretty date adapted from https://github.com/netcode/node-prettydate 96 | function createHandler(divisor, noun, restOfString){ 97 | return function(diff){ 98 | var n = Math.floor(diff/divisor); 99 | var pluralizedNoun = noun + ( n > 1 ? 's' : '' ); 100 | return "" + n + " " + pluralizedNoun + " " + restOfString; 101 | }; 102 | } 103 | 104 | var formatters = [ 105 | { threshold: 1, handler: function(){ return "just now"; } }, 106 | { threshold: 60, handler: createHandler(1, "second", "ago" ) }, 107 | { threshold: 3600, handler: createHandler(60, "minute", "ago" ) }, 108 | { threshold: 86400, handler: createHandler(3600, "hour", "ago" ) }, 109 | { threshold: 172800, handler: function(){ return "yesterday"; } }, 110 | { threshold: 604800, handler: createHandler(86400, "day", "ago" ) }, 111 | { threshold: 2592000, handler: createHandler(604800, "week", "ago" ) }, 112 | { threshold: 31536000, handler: createHandler(2592000, "month", "ago" ) }, 113 | { threshold: Infinity, handler: createHandler(31536000, "year", "ago" ) } 114 | ]; 115 | 116 | function prettyDate (date) { 117 | var diff = (((new Date()).getTime() - date.getTime()) / 1000); 118 | for( var i=0; i 2 | 3 | 4 | 5 | HN Plays 2048 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 |

Highscores

17 |
    18 |
    19 |
    20 |
    21 |

    HN Plays 2048

    22 |
    0
    23 |
    24 |

    Join the numbers and get to the 2048 tile!

    25 |

    1 user online

    26 | 27 |
    28 |
    29 |

    30 |
    31 | Good job! 32 |
    33 |
    34 | 35 |
    36 |
    37 |
    38 |
    39 |
    40 |
    41 |
    42 |
    43 |
    44 |
    45 |
    46 |
    47 |
    48 |
    49 |
    50 |
    51 |
    52 |
    53 |
    54 |
    55 |
    56 |
    57 |
    58 |
    59 |
    60 |
    61 | 62 |
    63 | 64 |
    65 |
    66 |

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

    69 |

    70 | However, this is HN plays 2048, which means anybody who is online can move your pieces! 71 |

    72 |
    73 | 79 | 88 |
    89 |
    90 |

    Inputs

    91 |
      92 |
      93 |
      94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 116 | 117 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var app = express(); 3 | var server = require('http').createServer(app); 4 | var io = require('socket.io').listen(server); 5 | 6 | app.use(express.static(__dirname + '/public')); 7 | 8 | io.configure('production', function(){ 9 | io.enable('browser client minification'); // send minified client 10 | io.enable('browser client etag'); // apply etag caching logic based on version number 11 | io.enable('browser client gzip'); // gzip the file 12 | io.set('log level', 1); // reduce logging 13 | // enable all transports (optional if you want flashsocket) 14 | io.set('transports', [ 'websocket', 'flashsocket', 'htmlfile', 'xhr-polling', 'jsonp-polling']); 15 | }); 16 | 17 | var port = process.env.PORT || 8000; 18 | server.listen(port); 19 | console.log("Listening at port: " + port); 20 | 21 | // Routes 22 | app.get('/api', function (req, res) { 23 | var data = game.getGameData(); 24 | data.highscores = game.getScores(); 25 | data.moveCount = moveCount; 26 | data.numUsers = io.sockets.clients().length; // Online users 27 | data.totalNumUsers = nextUserId; // Visitor count 28 | res.send(data); 29 | }); 30 | 31 | app.get('*', function (req, res) { 32 | res.sendfile(__dirname + '/index.html'); 33 | }); 34 | 35 | // Setup game 36 | var democracy = true; 37 | var nextUserId = 0; 38 | var moveCount = 0; 39 | var game = require('./private/js/game'); 40 | 41 | var voted = false; 42 | var votes = [0, 0, 0, 0]; // for democracy mode 43 | var ids = []; 44 | 45 | if (democracy) { 46 | setInterval(function() { 47 | var direction = 0; 48 | for (var i = 1; i < 4; i++) { 49 | if (votes[i] > votes[direction]) direction = i; 50 | } 51 | if (votes[direction] == 0) return; 52 | 53 | // COPIED FROM BELOW 54 | ++moveCount; 55 | // update the game 56 | game.move(direction); 57 | 58 | // Send the move with the game state 59 | var gameData = game.getGameData(); 60 | var data = { 61 | direction: direction, 62 | userId: "Democracy", 63 | numUsers: io.sockets.clients().length, 64 | gameData: gameData 65 | }; 66 | io.sockets.emit('move', data); 67 | 68 | // Reset the game if it is game over or won 69 | if (gameData.over || gameData.won) { 70 | game.restart(function () { 71 | var data = game.getGameData(); 72 | data.highscores = game.getHighscores(); 73 | io.sockets.emit('restart', data); 74 | }); 75 | } 76 | // END COPIED 77 | 78 | ids = []; 79 | votes = [0, 0, 0, 0]; 80 | voted = false; 81 | }, 1000); 82 | } 83 | 84 | io.sockets.on('connection', function (socket) { 85 | socket.userId = ++nextUserId; 86 | 87 | // When connecting 88 | var gameData = game.getGameData(); 89 | var data = { 90 | userId: socket.userId, 91 | gameData: gameData, 92 | numUsers: io.sockets.clients().length, 93 | highscores: game.getHighscores() 94 | }; 95 | socket.emit('connected', data); 96 | socket.broadcast.emit('someone connected', { 97 | numUsers: io.sockets.clients().length 98 | }); 99 | 100 | // When someone moves 101 | var numMovesPerSecond = 2; 102 | var pastEvents = []; 103 | for (var i = 0; i < numMovesPerSecond; i++) { 104 | pastEvents.push(0); 105 | } 106 | socket.on('move', function (direction) { 107 | if (democracy) { 108 | // Keep track of events 109 | pastEvents.push(new Date().getTime()); 110 | pastEvents.splice(0, pastEvents.length - numMovesPerSecond); 111 | 112 | // Multiplayer 113 | var spamming = pastEvents[pastEvents.length - 1] - pastEvents[0] < 1000; 114 | if (!voted && !spamming) { 115 | voted = true; 116 | votes[direction]++; 117 | 118 | // Send the move with the same old game state 119 | var gameData = game.getGameData(); 120 | var data = { 121 | direction: direction, 122 | userId: socket.userId, 123 | numUsers: io.sockets.clients().length, 124 | gameData: gameData 125 | }; 126 | io.sockets.emit('move', data); 127 | } 128 | } else { 129 | ++moveCount; 130 | // update the game 131 | game.move(direction); 132 | 133 | // Send the move with the game state 134 | var gameData = game.getGameData(); 135 | var data = { 136 | direction: direction, 137 | userId: socket.userId, 138 | numUsers: io.sockets.clients().length, 139 | gameData: gameData 140 | }; 141 | io.sockets.emit('move', data); 142 | 143 | // Reset the game if it is game over or won 144 | if (gameData.over || gameData.won) { 145 | game.restart(function () { 146 | var data = game.getGameData(); 147 | data.highscores = game.getHighscores(); 148 | io.sockets.emit('restart', data); 149 | }); 150 | } 151 | } 152 | }); 153 | 154 | socket.on('disconnect', function () { 155 | io.sockets.emit('someone disconnected', { 156 | numUsers: io.sockets.clients().length, 157 | }); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /public/js/game_manager.js: -------------------------------------------------------------------------------- 1 | function GameManager(size, InputManager, Actuator) { 2 | this.size = size; // Size of the grid 3 | this.inputManager = new InputManager; 4 | this.actuator = new Actuator; 5 | 6 | this.startTiles = 2; 7 | 8 | this.inputManager.on("move", this.move.bind(this)); 9 | this.inputManager.on("restart", this.restart.bind(this)); 10 | 11 | this.setup(); 12 | } 13 | 14 | // Restart the game 15 | GameManager.prototype.restart = function () { 16 | this.actuator.restart(); 17 | this.setup(); 18 | }; 19 | 20 | // Set up the game 21 | GameManager.prototype.setup = function () { 22 | this.grid = new Grid(this.size); 23 | 24 | this.score = 0; 25 | this.over = false; 26 | this.won = false; 27 | 28 | // Add the initial tiles 29 | // this.addStartTiles(); 30 | 31 | // Update the actuator 32 | this.actuate(); 33 | }; 34 | 35 | // Sends the updated grid to the actuator 36 | GameManager.prototype.actuate = function () { 37 | this.actuator.actuate(this.grid, { 38 | score: this.score, 39 | over: this.over, 40 | won: this.won 41 | }); 42 | }; 43 | 44 | // Save all tile positions and remove merger info 45 | GameManager.prototype.prepareTiles = function () { 46 | this.grid.eachCell(function (x, y, tile) { 47 | if (tile) { 48 | tile.mergedFrom = null; 49 | tile.savePosition(); 50 | } 51 | }); 52 | }; 53 | 54 | // Move a tile and its representation 55 | GameManager.prototype.moveTile = function (tile, cell) { 56 | this.grid.cells[tile.x][tile.y] = null; 57 | this.grid.cells[cell.x][cell.y] = tile; 58 | tile.updatePosition(cell); 59 | }; 60 | 61 | // Move tiles on the grid in the specified direction 62 | GameManager.prototype.move = function (direction) { 63 | // 0: up, 1: right, 2:down, 3: left 64 | var self = this; 65 | 66 | if (this.over || this.won) return; // Don't do anything if the game's over 67 | 68 | var cell, tile; 69 | 70 | var vector = this.getVector(direction); 71 | var traversals = this.buildTraversals(vector); 72 | var moved = false; 73 | 74 | // Save the current tile positions and remove merger information 75 | this.prepareTiles(); 76 | 77 | // Traverse the grid in the right direction and move tiles 78 | traversals.x.forEach(function (x) { 79 | traversals.y.forEach(function (y) { 80 | cell = { x: x, y: y }; 81 | tile = self.grid.cellContent(cell); 82 | 83 | if (tile) { 84 | var positions = self.findFarthestPosition(cell, vector); 85 | var next = self.grid.cellContent(positions.next); 86 | 87 | // Only one merger per row traversal? 88 | if (next && next.value === tile.value && !next.mergedFrom) { 89 | var merged = new Tile(positions.next, tile.value * 2); 90 | merged.mergedFrom = [tile, next]; 91 | 92 | self.grid.insertTile(merged); 93 | self.grid.removeTile(tile); 94 | 95 | // Converge the two tiles' positions 96 | tile.updatePosition(positions.next); 97 | 98 | // Update the score 99 | self.score += merged.value; 100 | 101 | // The mighty 2048 tile 102 | if (merged.value === 2048) self.won = true; 103 | } else { 104 | self.moveTile(tile, positions.farthest); 105 | } 106 | 107 | if (!self.positionsEqual(cell, tile)) { 108 | moved = true; // The tile moved from its original cell! 109 | } 110 | } 111 | }); 112 | }); 113 | 114 | if (moved) { 115 | this.addRandomTile(); 116 | 117 | if (!this.movesAvailable()) { 118 | this.over = true; // Game over! 119 | } 120 | 121 | this.actuate(); 122 | } 123 | }; 124 | 125 | // Get the vector representing the chosen direction 126 | GameManager.prototype.getVector = function (direction) { 127 | // Vectors representing tile movement 128 | var map = { 129 | 0: { x: 0, y: -1 }, // up 130 | 1: { x: 1, y: 0 }, // right 131 | 2: { x: 0, y: 1 }, // down 132 | 3: { x: -1, y: 0 } // left 133 | }; 134 | 135 | return map[direction]; 136 | }; 137 | 138 | // Build a list of positions to traverse in the right order 139 | GameManager.prototype.buildTraversals = function (vector) { 140 | var traversals = { x: [], y: [] }; 141 | 142 | for (var pos = 0; pos < this.size; pos++) { 143 | traversals.x.push(pos); 144 | traversals.y.push(pos); 145 | } 146 | 147 | // Always traverse from the farthest cell in the chosen direction 148 | if (vector.x === 1) traversals.x = traversals.x.reverse(); 149 | if (vector.y === 1) traversals.y = traversals.y.reverse(); 150 | 151 | return traversals; 152 | }; 153 | 154 | GameManager.prototype.findFarthestPosition = function (cell, vector) { 155 | var previous; 156 | 157 | // Progress towards the vector direction until an obstacle is found 158 | do { 159 | previous = cell; 160 | cell = { x: previous.x + vector.x, y: previous.y + vector.y }; 161 | } while (this.grid.withinBounds(cell) && 162 | this.grid.cellAvailable(cell)); 163 | 164 | return { 165 | farthest: previous, 166 | next: cell // Used to check if a merge is required 167 | }; 168 | }; 169 | 170 | GameManager.prototype.movesAvailable = function () { 171 | return this.grid.cellsAvailable() || this.tileMatchesAvailable(); 172 | }; 173 | 174 | // Check for available matches between tiles (more expensive check) 175 | GameManager.prototype.tileMatchesAvailable = function () { 176 | var self = this; 177 | 178 | var tile; 179 | 180 | for (var x = 0; x < this.size; x++) { 181 | for (var y = 0; y < this.size; y++) { 182 | tile = this.grid.cellContent({ x: x, y: y }); 183 | 184 | if (tile) { 185 | for (var direction = 0; direction < 4; direction++) { 186 | var vector = self.getVector(direction); 187 | var cell = { x: x + vector.x, y: y + vector.y }; 188 | 189 | var other = self.grid.cellContent(cell); 190 | if (other) { 191 | } 192 | 193 | if (other && other.value === tile.value) { 194 | return true; // These two tiles can be merged 195 | } 196 | } 197 | } 198 | } 199 | } 200 | 201 | return false; 202 | }; 203 | 204 | GameManager.prototype.positionsEqual = function (first, second) { 205 | return first.x === second.x && first.y === second.y; 206 | }; 207 | 208 | GameManager.prototype.setGameData = function (data) { 209 | this.grid = data.grid; 210 | this.won = data.won; 211 | this.over = data.over; 212 | this.score = data.score; 213 | 214 | this.actuate(); 215 | }; -------------------------------------------------------------------------------- /private/js/game_manager.js: -------------------------------------------------------------------------------- 1 | // Requires 2 | var Tile = require('./tile'); 3 | var Grid = require('./grid'); 4 | 5 | function GameManager(size) { 6 | this.size = size; // Size of the grid 7 | this.startTiles = 2; 8 | 9 | this.setup(); 10 | } 11 | 12 | // Restart the game 13 | GameManager.prototype.restart = function () { 14 | this.setup(); 15 | }; 16 | 17 | // Set up the game 18 | GameManager.prototype.setup = function () { 19 | this.grid = new Grid(this.size); 20 | 21 | this.score = 0; 22 | this.over = false; 23 | this.won = false; 24 | 25 | // Add the initial tiles 26 | this.addStartTiles(); 27 | 28 | // Update the actuator 29 | this.actuate(); 30 | }; 31 | 32 | // Set up the initial tiles to start the game with 33 | GameManager.prototype.addStartTiles = function () { 34 | for (var i = 0; i < this.startTiles; i++) { 35 | this.addRandomTile(); 36 | } 37 | }; 38 | 39 | // Adds a tile in a random position 40 | GameManager.prototype.addRandomTile = function () { 41 | if (this.grid.cellsAvailable()) { 42 | var value = Math.random() < 0.9 ? 2 : 4; 43 | var tile = new Tile(this.grid.randomAvailableCell(), value); 44 | 45 | this.grid.insertTile(tile); 46 | } 47 | }; 48 | 49 | // Sends the updated grid to the actuator 50 | GameManager.prototype.actuate = function () { 51 | this.data = { 52 | grid: this.grid, 53 | score: this.score, 54 | over: this.over, 55 | won: this.won 56 | }; 57 | }; 58 | 59 | // Save all tile positions and remove merger info 60 | GameManager.prototype.prepareTiles = function () { 61 | this.grid.eachCell(function (x, y, tile) { 62 | if (tile) { 63 | tile.mergedFrom = null; 64 | tile.savePosition(); 65 | } 66 | }); 67 | }; 68 | 69 | // Move a tile and its representation 70 | GameManager.prototype.moveTile = function (tile, cell) { 71 | this.grid.cells[tile.x][tile.y] = null; 72 | this.grid.cells[cell.x][cell.y] = tile; 73 | tile.updatePosition(cell); 74 | }; 75 | 76 | // Move tiles on the grid in the specified direction 77 | GameManager.prototype.move = function (direction) { 78 | // 0: up, 1: right, 2:down, 3: left 79 | var self = this; 80 | 81 | if (this.over || this.won) return; // Don't do anything if the game's over 82 | 83 | var cell, tile; 84 | 85 | var vector = this.getVector(direction); 86 | var traversals = this.buildTraversals(vector); 87 | var moved = false; 88 | 89 | // Save the current tile positions and remove merger information 90 | this.prepareTiles(); 91 | 92 | // Traverse the grid in the right direction and move tiles 93 | traversals.x.forEach(function (x) { 94 | traversals.y.forEach(function (y) { 95 | cell = { x: x, y: y }; 96 | tile = self.grid.cellContent(cell); 97 | 98 | if (tile) { 99 | var positions = self.findFarthestPosition(cell, vector); 100 | var next = self.grid.cellContent(positions.next); 101 | 102 | // Only one merger per row traversal? 103 | if (next && next.value === tile.value && !next.mergedFrom) { 104 | var merged = new Tile(positions.next, tile.value * 2); 105 | merged.mergedFrom = [tile, next]; 106 | 107 | self.grid.insertTile(merged); 108 | self.grid.removeTile(tile); 109 | 110 | // Converge the two tiles' positions 111 | tile.updatePosition(positions.next); 112 | 113 | // Update the score 114 | self.score += merged.value; 115 | 116 | // The mighty 2048 tile 117 | if (merged.value === 2048) self.won = true; 118 | } else { 119 | self.moveTile(tile, positions.farthest); 120 | } 121 | 122 | if (!self.positionsEqual(cell, tile)) { 123 | moved = true; // The tile moved from its original cell! 124 | } 125 | } 126 | }); 127 | }); 128 | 129 | if (moved) { 130 | this.addRandomTile(); 131 | 132 | if (!this.movesAvailable()) { 133 | this.over = true; // Game over! 134 | } 135 | 136 | this.actuate(); 137 | } 138 | }; 139 | 140 | // Get the vector representing the chosen direction 141 | GameManager.prototype.getVector = function (direction) { 142 | // Vectors representing tile movement 143 | var map = { 144 | 0: { x: 0, y: -1 }, // up 145 | 1: { x: 1, y: 0 }, // right 146 | 2: { x: 0, y: 1 }, // down 147 | 3: { x: -1, y: 0 } // left 148 | }; 149 | 150 | return map[direction]; 151 | }; 152 | 153 | // Build a list of positions to traverse in the right order 154 | GameManager.prototype.buildTraversals = function (vector) { 155 | var traversals = { x: [], y: [] }; 156 | 157 | for (var pos = 0; pos < this.size; pos++) { 158 | traversals.x.push(pos); 159 | traversals.y.push(pos); 160 | } 161 | 162 | // Always traverse from the farthest cell in the chosen direction 163 | if (vector.x === 1) traversals.x = traversals.x.reverse(); 164 | if (vector.y === 1) traversals.y = traversals.y.reverse(); 165 | 166 | return traversals; 167 | }; 168 | 169 | GameManager.prototype.findFarthestPosition = function (cell, vector) { 170 | var previous; 171 | 172 | // Progress towards the vector direction until an obstacle is found 173 | do { 174 | previous = cell; 175 | cell = { x: previous.x + vector.x, y: previous.y + vector.y }; 176 | } while (this.grid.withinBounds(cell) && 177 | this.grid.cellAvailable(cell)); 178 | 179 | return { 180 | farthest: previous, 181 | next: cell // Used to check if a merge is required 182 | }; 183 | }; 184 | 185 | GameManager.prototype.movesAvailable = function () { 186 | return this.grid.cellsAvailable() || this.tileMatchesAvailable(); 187 | }; 188 | 189 | // Check for available matches between tiles (more expensive check) 190 | GameManager.prototype.tileMatchesAvailable = function () { 191 | var self = this; 192 | 193 | var tile; 194 | 195 | for (var x = 0; x < this.size; x++) { 196 | for (var y = 0; y < this.size; y++) { 197 | tile = this.grid.cellContent({ x: x, y: y }); 198 | 199 | if (tile) { 200 | for (var direction = 0; direction < 4; direction++) { 201 | var vector = self.getVector(direction); 202 | var cell = { x: x + vector.x, y: y + vector.y }; 203 | 204 | var other = self.grid.cellContent(cell); 205 | if (other) { 206 | } 207 | 208 | if (other && other.value === tile.value) { 209 | return true; // These two tiles can be merged 210 | } 211 | } 212 | } 213 | } 214 | } 215 | 216 | return false; 217 | }; 218 | 219 | GameManager.prototype.positionsEqual = function (first, second) { 220 | return first.x === second.x && first.y === second.y; 221 | }; 222 | 223 | GameManager.prototype.getGameData = function () { 224 | return this.data; 225 | }; 226 | 227 | module.exports = GameManager; -------------------------------------------------------------------------------- /private/sass/main.sass: -------------------------------------------------------------------------------- 1 | @import helpers 2 | @import fonts/clear-sans.css 3 | 4 | $column-width: 240px 5 | $field-width: 500px 6 | $grid-spacing: 15px 7 | $grid-row-cells: 4 8 | $tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells 9 | $tile-border-radius: 3px 10 | 11 | $text-color: #776e65 12 | $bright-text-color: #f9f6f2 13 | 14 | $tile-color: #eee4da 15 | $tile-gold-color: #edc22e 16 | $tile-gold-glow-color: lighten($tile-gold-color, 15%) 17 | 18 | $game-container-background: #bbada0 19 | 20 | $transition-speed: 100ms 21 | 22 | html, body 23 | margin: 0 24 | padding: 0 25 | background: #faf8ef 26 | color: $text-color 27 | font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif 28 | font-size: 18px 29 | 30 | body 31 | margin: 20px 0 32 | 33 | ul, ol 34 | list-style-type: none 35 | padding: 0 36 | 37 | p 38 | margin: 0 39 | line-height: 1.65 40 | 41 | a 42 | color: $text-color 43 | font-weight: bold 44 | text-decoration: underline 45 | cursor: pointer 46 | 47 | strong 48 | &.important 49 | text-transform: uppercase 50 | 51 | hr 52 | border: none 53 | border-bottom: 1px solid lighten($text-color, 40%) 54 | margin: 10px 0 55 | 56 | .heading:after 57 | content: "" 58 | display: block 59 | clear: both 60 | 61 | h1.title 62 | font-size: 40px 63 | font-weight: bold 64 | margin: 0 65 | display: block 66 | float: left 67 | 68 | +keyframes(move-up) 69 | 0% 70 | top: 25px 71 | opacity: 1 72 | 100% 73 | top: -50px 74 | opacity: 0 75 | 76 | 77 | .score-container 78 | $height: 25px 79 | position: relative 80 | float: right 81 | background: $game-container-background 82 | padding: 15px 25px 83 | font-size: $height 84 | height: $height 85 | line-height: $height + 22px 86 | font-weight: bold 87 | border-radius: 3px 88 | color: white 89 | margin-top: 0 90 | &:after 91 | position: absolute 92 | width: 100% 93 | top: 10px 94 | left: 0 95 | content: "Score" 96 | text-transform: uppercase 97 | font-size: 13px 98 | line-height: 13px 99 | text-align: center 100 | color: $tile-color 101 | .score-addition 102 | position: absolute 103 | right: 30px 104 | color: red 105 | font-size: $height 106 | line-height: $height 107 | font-weight: bold 108 | color: rgba($text-color, 0.9) 109 | z-index: 100 110 | +animation(move-up 600ms ease-in) 111 | +animation-fill-mode(both) 112 | 113 | .container 114 | width: $field-width + 2 * $column-width 115 | margin: 0 auto 116 | 117 | > div 118 | float: left 119 | 120 | .game 121 | width: $field-width 122 | 123 | .highscores, .userInputs 124 | width: $column-width 125 | text-align: center 126 | 127 | .highscores 128 | li 129 | margin-bottom: 10px 130 | .score 131 | padding-right: 5px 132 | .won 133 | color: #edcf72 134 | 135 | .userInputs 136 | li 137 | margin-bottom: 10px 138 | .move 139 | display: inline-block 140 | width: 20px 141 | height: 20px 142 | 143 | font-size: 20px 144 | color: $bright-text-color 145 | 146 | margin-right: 5px 147 | padding: 5px 148 | border-radius: 5px 149 | .move-0 150 | background-color: #edcf72 151 | .move-1 152 | background-color: #f2b179 153 | .move-2 154 | background-color: #ede0c8 155 | .move-3 156 | background-color: #f67c5f 157 | 158 | +keyframes(fade-in) 159 | 0% 160 | opacity: 0 161 | 100% 162 | opacity: 1 163 | 164 | 165 | // Game field mixin used to render CSS at different width 166 | =game-field 167 | .game-container 168 | margin-top: 20px 169 | position: relative 170 | padding: $grid-spacing 171 | cursor: default 172 | -webkit-touch-callout: none 173 | -webkit-user-select: none 174 | -moz-user-select: none 175 | background: $game-container-background 176 | border-radius: $tile-border-radius * 2 177 | width: $field-width 178 | height: $field-width 179 | -webkit-box-sizing: border-box 180 | -moz-box-sizing: border-box 181 | box-sizing: border-box 182 | .game-message 183 | display: none 184 | position: absolute 185 | top: 0 186 | right: 0 187 | bottom: 0 188 | left: 0 189 | background: rgba($tile-color, 0.5) 190 | z-index: 100 191 | text-align: center 192 | p 193 | font-size: 60px 194 | font-weight: bold 195 | height: 60px 196 | line-height: 60px 197 | margin-top: 222px 198 | // height: $field-width; 199 | // line-height: $field-width; 200 | .lower 201 | display: block 202 | margin-top: 59px 203 | a 204 | display: inline-block 205 | background: darken($game-container-background, 20%) 206 | border-radius: 3px 207 | padding: 0 20px 208 | text-decoration: none 209 | color: $bright-text-color 210 | height: 40px 211 | line-height: 42px 212 | margin-left: 9px 213 | // margin-top: 59px; 214 | +animation(fade-in 800ms ease $transition-speed * 12) 215 | +animation-fill-mode(both) 216 | &.game-won 217 | background: rgba($tile-gold-color, 0.5) 218 | color: $bright-text-color 219 | &.game-won, &.game-over 220 | display: block 221 | .grid-container 222 | position: absolute 223 | z-index: 1 224 | .grid-row 225 | margin-bottom: $grid-spacing 226 | &:last-child 227 | margin-bottom: 0 228 | &:after 229 | content: "" 230 | display: block 231 | clear: both 232 | .grid-cell 233 | width: $tile-size 234 | height: $tile-size 235 | margin-right: $grid-spacing 236 | float: left 237 | border-radius: $tile-border-radius 238 | background: rgba($tile-color, 0.35) 239 | &:last-child 240 | margin-right: 0 241 | .tile-container 242 | position: absolute 243 | z-index: 2 244 | .tile 245 | width: $tile-size 246 | height: $tile-size 247 | line-height: $tile-size + 10px 248 | // Build position classes 249 | @for $x from 1 through $grid-row-cells 250 | @for $y from 1 through $grid-row-cells 251 | &.tile-position-#{$x}-#{$y} 252 | position: absolute 253 | left: round(($tile-size + $grid-spacing) * ($x - 1)) 254 | top: round(($tile-size + $grid-spacing) * ($y - 1)) 255 | 256 | // End of game-field mixin 257 | +game-field 258 | 259 | .tile 260 | border-radius: $tile-border-radius 261 | background: $tile-color 262 | text-align: center 263 | font-weight: bold 264 | z-index: 10 265 | font-size: 55px 266 | +transition($transition-speed ease-in-out) 267 | +transition-property(top, left) 268 | $base: 2 269 | $exponent: 1 270 | $limit: 11 271 | // Colors for all 11 states, false = no special color 272 | $special-colors: false false, false false, #f78e48 true, #fc5e2e true, #ff3333 true, red true, false true, false true, false true, false true, false true 273 | // 2048 274 | // Build tile colors 275 | @while $exponent <= $limit 276 | $power: pow($base, $exponent) 277 | &.tile-#{$power} 278 | // Calculate base background color 279 | $gold-percent: ($exponent - 1) / ($limit - 1) * 100 280 | $mixed-background: mix($tile-gold-color, $tile-color, $gold-percent) 281 | $nth-color: nth($special-colors, $exponent) 282 | $special-background: nth($nth-color, 1) 283 | $bright-color: nth($nth-color, 2) 284 | @if $special-background 285 | $mixed-background: mix($special-background, $mixed-background, 55%) 286 | @if $bright-color 287 | color: $bright-text-color 288 | // Set background 289 | background: $mixed-background 290 | // Add glow 291 | $glow-opacity: max($exponent - 4, 0) / ($limit - 4) 292 | @if not $special-background 293 | box-shadow: 0 0 30px 10px rgba($tile-gold-glow-color, $glow-opacity / 1.8), inset 0 0 0 1px rgba(white, $glow-opacity / 3) 294 | // Adjust font size for bigger numbers 295 | @if $power >= 100 and $power < 1000 296 | font-size: 45px 297 | // Media queries placed here to avoid carrying over the rest of the logic 298 | +smaller(480px) 299 | font-size: 25px 300 | @else if $power >= 1000 301 | font-size: 35px 302 | +smaller(480px) 303 | font-size: 15px 304 | $exponent: $exponent + 1 305 | 306 | +keyframes(appear) 307 | 0% 308 | opacity: 0 309 | -webkit-transform: scale(0) 310 | -moz-transform: scale(0) 311 | 100% 312 | opacity: 1 313 | -webkit-transform: scale(1) 314 | -moz-transform: scale(1) 315 | 316 | 317 | .tile-new 318 | +animation(appear 200ms ease $transition-speed) 319 | +animation-fill-mode(both) 320 | 321 | +keyframes(pop) 322 | 0% 323 | -webkit-transform: scale(0) 324 | -moz-transform: scale(0) 325 | 50% 326 | -webkit-transform: scale(1.2) 327 | -moz-transform: scale(1.2) 328 | 100% 329 | -webkit-transform: scale(1) 330 | -moz-transform: scale(1) 331 | 332 | 333 | .tile-merged 334 | z-index: 20 335 | +animation(pop 200ms ease $transition-speed) 336 | +animation-fill-mode(both) 337 | 338 | .game-intro 339 | margin-bottom: 0 340 | 341 | .game-explanation 342 | margin-top: 20px 343 | 344 | .social 345 | text-align: center 346 | 347 | .links 348 | text-align: center 349 | li 350 | margin-bottom: 5px 351 | 352 | +smaller(480px) 353 | // Redefine variables for smaller screens 354 | $field-width: 280px 355 | $grid-spacing: 10px 356 | $grid-row-cells: 4 357 | $tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells 358 | $tile-border-radius: 3px 359 | html, body 360 | font-size: 15px 361 | body 362 | margin: 20px 0 363 | padding: 0 20px 364 | h1.title 365 | font-size: 50px 366 | .container 367 | width: $field-width 368 | margin: 0 auto 369 | .score-container 370 | margin-top: 0 371 | .heading 372 | margin-bottom: 10px 373 | // Render the game field at the right width 374 | +game-field 375 | .game-container 376 | margin-top: 20px 377 | // Rest of the font-size adjustments in the tile class 378 | .tile 379 | font-size: 35px 380 | .game-message 381 | p 382 | font-size: 30px !important 383 | height: 30px !important 384 | line-height: 30px !important 385 | margin-top: 90px !important 386 | .lower 387 | margin-top: 30px !important -------------------------------------------------------------------------------- /public/style/main.css: -------------------------------------------------------------------------------- 1 | @import url(fonts/clear-sans.css);html,body{margin:0;padding:0;background:#faf8ef;color:#776e65;font-family:"Clear Sans","Helvetica Neue",Arial,sans-serif;font-size:18px}body{margin:20px 0}ul,ol{list-style-type:none;padding:0}p{margin:0;line-height:1.65}a{color:#776e65;font-weight:bold;text-decoration:underline;cursor:pointer}strong.important{text-transform:uppercase}hr{border:none;border-bottom:1px solid #d8d4d0;margin:10px 0}.heading:after{content:"";display:block;clear:both}h1.title{font-size:40px;font-weight:bold;margin:0;display:block;float:left}@-webkit-keyframes move-up{0%{top:25px;opacity:1}100%{top:-50px;opacity:0}}@-moz-keyframes move-up{0%{top:25px;opacity:1}100%{top:-50px;opacity:0}}@keyframes move-up{0%{top:25px;opacity:1}100%{top:-50px;opacity:0}}.score-container{position:relative;float:right;background:#bbada0;padding:15px 25px;font-size:25px;height:25px;line-height:47px;font-weight:bold;border-radius:3px;color:#fff;margin-top:0}.score-container:after{position:absolute;width:100%;top:10px;left:0;content:"Score";text-transform:uppercase;font-size:13px;line-height:13px;text-align:center;color:#eee4da}.score-container .score-addition{position:absolute;right:30px;color:red;font-size:25px;line-height:25px;font-weight:bold;color:rgba(119,110,101,0.9);z-index:100;-webkit-animation:move-up 600ms ease-in;-moz-animation:move-up 600ms ease-in;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both}.container{width:980px;margin:0 auto}.container>div{float:left}.container .game{width:500px}.container .highscores,.container .userInputs{width:240px;text-align:center}.highscores li{margin-bottom:10px}.highscores .score{padding-right:5px}.highscores .won{color:#edcf72}.userInputs li{margin-bottom:10px}.userInputs .move{display:inline-block;width:20px;height:20px;font-size:20px;color:#f9f6f2;margin-right:5px;padding:5px;border-radius:5px}.userInputs .move-0{background-color:#edcf72}.userInputs .move-1{background-color:#f2b179}.userInputs .move-2{background-color:#ede0c8}.userInputs .move-3{background-color:#f67c5f}@-webkit-keyframes fade-in{0%{opacity:0}100%{opacity:1}}@-moz-keyframes fade-in{0%{opacity:0}100%{opacity:1}}@keyframes fade-in{0%{opacity:0}100%{opacity:1}}.game-container{margin-top:20px;position:relative;padding:15px;cursor:default;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;background:#bbada0;border-radius:6px;width:500px;height:500px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.game-container .game-message{display:none;position:absolute;top:0;right:0;bottom:0;left:0;background:rgba(238,228,218,0.5);z-index:100;text-align:center;-webkit-animation:fade-in 800ms ease 1200ms;-moz-animation:fade-in 800ms ease 1200ms;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both}.game-container .game-message p{font-size:60px;font-weight:bold;height:60px;line-height:60px;margin-top:222px}.game-container .game-message .lower{display:block;margin-top:59px}.game-container .game-message a{display:inline-block;background:#8f7a66;border-radius:3px;padding:0 20px;text-decoration:none;color:#f9f6f2;height:40px;line-height:42px;margin-left:9px}.game-container .game-message.game-won{background:rgba(237,194,46,0.5);color:#f9f6f2}.game-container .game-message.game-won,.game-container .game-message.game-over{display:block}.grid-container{position:absolute;z-index:1}.grid-row{margin-bottom:15px}.grid-row:last-child{margin-bottom:0}.grid-row:after{content:"";display:block;clear:both}.grid-cell{width:106.25px;height:106.25px;margin-right:15px;float:left;border-radius:3px;background:rgba(238,228,218,0.35)}.grid-cell:last-child{margin-right:0}.tile-container{position:absolute;z-index:2}.tile{width:106.25px;height:106.25px;line-height:116.25px}.tile.tile-position-1-1{position:absolute;left:0px;top:0px}.tile.tile-position-1-2{position:absolute;left:0px;top:121px}.tile.tile-position-1-3{position:absolute;left:0px;top:243px}.tile.tile-position-1-4{position:absolute;left:0px;top:364px}.tile.tile-position-2-1{position:absolute;left:121px;top:0px}.tile.tile-position-2-2{position:absolute;left:121px;top:121px}.tile.tile-position-2-3{position:absolute;left:121px;top:243px}.tile.tile-position-2-4{position:absolute;left:121px;top:364px}.tile.tile-position-3-1{position:absolute;left:243px;top:0px}.tile.tile-position-3-2{position:absolute;left:243px;top:121px}.tile.tile-position-3-3{position:absolute;left:243px;top:243px}.tile.tile-position-3-4{position:absolute;left:243px;top:364px}.tile.tile-position-4-1{position:absolute;left:364px;top:0px}.tile.tile-position-4-2{position:absolute;left:364px;top:121px}.tile.tile-position-4-3{position:absolute;left:364px;top:243px}.tile.tile-position-4-4{position:absolute;left:364px;top:364px}.tile{border-radius:3px;background:#eee4da;text-align:center;font-weight:bold;z-index:10;font-size:55px;-webkit-transition:100ms ease-in-out;-moz-transition:100ms ease-in-out;-webkit-transition-property:top,left;-moz-transition-property:top,left}.tile.tile-2{background:#eee4da;box-shadow:0 0 30px 10px rgba(243,215,116,0),inset 0 0 0 1px rgba(255,255,255,0)}.tile.tile-4{background:#ede0c8;box-shadow:0 0 30px 10px rgba(243,215,116,0),inset 0 0 0 1px rgba(255,255,255,0)}.tile.tile-8{color:#f9f6f2;background:#f2b179}.tile.tile-16{color:#f9f6f2;background:#f59563}.tile.tile-32{color:#f9f6f2;background:#f67c5f}.tile.tile-64{color:#f9f6f2;background:#f65e3b}.tile.tile-128{color:#f9f6f2;background:#edcf72;box-shadow:0 0 30px 10px rgba(243,215,116,0.2381),inset 0 0 0 1px rgba(255,255,255,0.14286);font-size:45px}@media screen and (max-width: 480px){.tile.tile-128{font-size:25px}}.tile.tile-256{color:#f9f6f2;background:#edcc61;box-shadow:0 0 30px 10px rgba(243,215,116,0.31746),inset 0 0 0 1px rgba(255,255,255,0.19048);font-size:45px}@media screen and (max-width: 480px){.tile.tile-256{font-size:25px}}.tile.tile-512{color:#f9f6f2;background:#edc850;box-shadow:0 0 30px 10px rgba(243,215,116,0.39683),inset 0 0 0 1px rgba(255,255,255,0.2381);font-size:45px}@media screen and (max-width: 480px){.tile.tile-512{font-size:25px}}.tile.tile-1024{color:#f9f6f2;background:#edc53f;box-shadow:0 0 30px 10px rgba(243,215,116,0.47619),inset 0 0 0 1px rgba(255,255,255,0.28571);font-size:35px}@media screen and (max-width: 480px){.tile.tile-1024{font-size:15px}}.tile.tile-2048{color:#f9f6f2;background:#edc22e;box-shadow:0 0 30px 10px rgba(243,215,116,0.55556),inset 0 0 0 1px rgba(255,255,255,0.33333);font-size:35px}@media screen and (max-width: 480px){.tile.tile-2048{font-size:15px}} 2 | @-webkit-keyframes appear{0%{opacity:0;-webkit-transform:scale(0);-moz-transform:scale(0)}100%{opacity:1;-webkit-transform:scale(1);-moz-transform:scale(1)}}@-moz-keyframes appear{0%{opacity:0;-webkit-transform:scale(0);-moz-transform:scale(0)}100%{opacity:1;-webkit-transform:scale(1);-moz-transform:scale(1)}}@keyframes appear{0%{opacity:0;-webkit-transform:scale(0);-moz-transform:scale(0)}100%{opacity:1;-webkit-transform:scale(1);-moz-transform:scale(1)}}.tile-new{-webkit-animation:appear 200ms ease 100ms;-moz-animation:appear 200ms ease 100ms;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both}@-webkit-keyframes pop{0%{-webkit-transform:scale(0);-moz-transform:scale(0)}50%{-webkit-transform:scale(1.2);-moz-transform:scale(1.2)}100%{-webkit-transform:scale(1);-moz-transform:scale(1)}}@-moz-keyframes pop{0%{-webkit-transform:scale(0);-moz-transform:scale(0)}50%{-webkit-transform:scale(1.2);-moz-transform:scale(1.2)}100%{-webkit-transform:scale(1);-moz-transform:scale(1)}}@keyframes pop{0%{-webkit-transform:scale(0);-moz-transform:scale(0)}50%{-webkit-transform:scale(1.2);-moz-transform:scale(1.2)}100%{-webkit-transform:scale(1);-moz-transform:scale(1)}}.tile-merged{z-index:20;-webkit-animation:pop 200ms ease 100ms;-moz-animation:pop 200ms ease 100ms;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both}.game-intro{margin-bottom:0}.game-explanation{margin-top:20px}.social{text-align:center}.links{text-align:center}.links li{margin-bottom:5px}@media screen and (max-width: 480px){html,body{font-size:15px}body{margin:20px 0;padding:0 20px}h1.title{font-size:50px}.container{width:280px;margin:0 auto}.score-container{margin-top:0}.heading{margin-bottom:10px}.game-container{margin-top:20px;position:relative;padding:10px;cursor:default;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;background:#bbada0;border-radius:6px;width:280px;height:280px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.game-container .game-message{display:none;position:absolute;top:0;right:0;bottom:0;left:0;background:rgba(238,228,218,0.5);z-index:100;text-align:center;-webkit-animation:fade-in 800ms ease 1200ms;-moz-animation:fade-in 800ms ease 1200ms;-webkit-animation-fill-mode:both;-moz-animation-fill-mode:both}.game-container .game-message p{font-size:60px;font-weight:bold;height:60px;line-height:60px;margin-top:222px}.game-container .game-message .lower{display:block;margin-top:59px}.game-container .game-message a{display:inline-block;background:#8f7a66;border-radius:3px;padding:0 20px;text-decoration:none;color:#f9f6f2;height:40px;line-height:42px;margin-left:9px}.game-container .game-message.game-won{background:rgba(237,194,46,0.5);color:#f9f6f2}.game-container .game-message.game-won,.game-container .game-message.game-over{display:block}.grid-container{position:absolute;z-index:1}.grid-row{margin-bottom:10px}.grid-row:last-child{margin-bottom:0}.grid-row:after{content:"";display:block;clear:both}.grid-cell{width:57.5px;height:57.5px;margin-right:10px;float:left;border-radius:3px;background:rgba(238,228,218,0.35)}.grid-cell:last-child{margin-right:0}.tile-container{position:absolute;z-index:2}.tile{width:57.5px;height:57.5px;line-height:67.5px}.tile.tile-position-1-1{position:absolute;left:0px;top:0px}.tile.tile-position-1-2{position:absolute;left:0px;top:68px}.tile.tile-position-1-3{position:absolute;left:0px;top:135px}.tile.tile-position-1-4{position:absolute;left:0px;top:203px}.tile.tile-position-2-1{position:absolute;left:68px;top:0px}.tile.tile-position-2-2{position:absolute;left:68px;top:68px}.tile.tile-position-2-3{position:absolute;left:68px;top:135px}.tile.tile-position-2-4{position:absolute;left:68px;top:203px}.tile.tile-position-3-1{position:absolute;left:135px;top:0px}.tile.tile-position-3-2{position:absolute;left:135px;top:68px}.tile.tile-position-3-3{position:absolute;left:135px;top:135px}.tile.tile-position-3-4{position:absolute;left:135px;top:203px}.tile.tile-position-4-1{position:absolute;left:203px;top:0px}.tile.tile-position-4-2{position:absolute;left:203px;top:68px}.tile.tile-position-4-3{position:absolute;left:203px;top:135px}.tile.tile-position-4-4{position:absolute;left:203px;top:203px}.game-container{margin-top:20px}.tile{font-size:35px}.game-message p{font-size:30px !important;height:30px !important;line-height:30px !important;margin-top:90px !important}.game-message .lower{margin-top:30px !important}} 3 | -------------------------------------------------------------------------------- /public/js/hammer.min.js: -------------------------------------------------------------------------------- 1 | /*! Hammer.JS - v1.0.6 - 2014-01-02 2 | * http://eightmedia.github.com/hammer.js 3 | * 4 | * Copyright (c) 2014 Jorik Tangelder ; 5 | * Licensed under the MIT license */ 6 | 7 | 8 | !function(a,b){"use strict";function c(){d.READY||(d.event.determineEventTypes(),d.utils.each(d.gestures,function(a){d.detection.register(a)}),d.event.onTouch(d.DOCUMENT,d.EVENT_MOVE,d.detection.detect),d.event.onTouch(d.DOCUMENT,d.EVENT_END,d.detection.detect),d.READY=!0)}var d=function(a,b){return new d.Instance(a,b||{})};d.defaults={stop_browser_behavior:{userSelect:"none",touchAction:"none",touchCallout:"none",contentZooming:"none",userDrag:"none",tapHighlightColor:"rgba(0,0,0,0)"}},d.HAS_POINTEREVENTS=a.navigator.pointerEnabled||a.navigator.msPointerEnabled,d.HAS_TOUCHEVENTS="ontouchstart"in a,d.MOBILE_REGEX=/mobile|tablet|ip(ad|hone|od)|android|silk/i,d.NO_MOUSEEVENTS=d.HAS_TOUCHEVENTS&&a.navigator.userAgent.match(d.MOBILE_REGEX),d.EVENT_TYPES={},d.DIRECTION_DOWN="down",d.DIRECTION_LEFT="left",d.DIRECTION_UP="up",d.DIRECTION_RIGHT="right",d.POINTER_MOUSE="mouse",d.POINTER_TOUCH="touch",d.POINTER_PEN="pen",d.EVENT_START="start",d.EVENT_MOVE="move",d.EVENT_END="end",d.DOCUMENT=a.document,d.plugins=d.plugins||{},d.gestures=d.gestures||{},d.READY=!1,d.utils={extend:function(a,c,d){for(var e in c)a[e]!==b&&d||(a[e]=c[e]);return a},each:function(a,c,d){var e,f;if("forEach"in a)a.forEach(c,d);else if(a.length!==b){for(e=0,f=a.length;f>e;e++)if(c.call(d,a[e],e,a)===!1)return}else for(e in a)if(a.hasOwnProperty(e)&&c.call(d,a[e],e,a)===!1)return},hasParent:function(a,b){for(;a;){if(a==b)return!0;a=a.parentNode}return!1},getCenter:function(a){var b=[],c=[];return d.utils.each(a,function(a){b.push("undefined"!=typeof a.clientX?a.clientX:a.pageX),c.push("undefined"!=typeof a.clientY?a.clientY:a.pageY)}),{pageX:(Math.min.apply(Math,b)+Math.max.apply(Math,b))/2,pageY:(Math.min.apply(Math,c)+Math.max.apply(Math,c))/2}},getVelocity:function(a,b,c){return{x:Math.abs(b/a)||0,y:Math.abs(c/a)||0}},getAngle:function(a,b){var c=b.pageY-a.pageY,d=b.pageX-a.pageX;return 180*Math.atan2(c,d)/Math.PI},getDirection:function(a,b){var c=Math.abs(a.pageX-b.pageX),e=Math.abs(a.pageY-b.pageY);return c>=e?a.pageX-b.pageX>0?d.DIRECTION_LEFT:d.DIRECTION_RIGHT:a.pageY-b.pageY>0?d.DIRECTION_UP:d.DIRECTION_DOWN},getDistance:function(a,b){var c=b.pageX-a.pageX,d=b.pageY-a.pageY;return Math.sqrt(c*c+d*d)},getScale:function(a,b){return a.length>=2&&b.length>=2?this.getDistance(b[0],b[1])/this.getDistance(a[0],a[1]):1},getRotation:function(a,b){return a.length>=2&&b.length>=2?this.getAngle(b[1],b[0])-this.getAngle(a[1],a[0]):0},isVertical:function(a){return a==d.DIRECTION_UP||a==d.DIRECTION_DOWN},stopDefaultBrowserBehavior:function(a,b){b&&a&&a.style&&(d.utils.each(["webkit","khtml","moz","Moz","ms","o",""],function(c){d.utils.each(b,function(b){c&&(b=c+b.substring(0,1).toUpperCase()+b.substring(1)),b in a.style&&(a.style[b]=b)})}),"none"==b.userSelect&&(a.onselectstart=function(){return!1}),"none"==b.userDrag&&(a.ondragstart=function(){return!1}))}},d.Instance=function(a,b){var e=this;return c(),this.element=a,this.enabled=!0,this.options=d.utils.extend(d.utils.extend({},d.defaults),b||{}),this.options.stop_browser_behavior&&d.utils.stopDefaultBrowserBehavior(this.element,this.options.stop_browser_behavior),d.event.onTouch(a,d.EVENT_START,function(a){e.enabled&&d.detection.startDetect(e,a)}),this},d.Instance.prototype={on:function(a,b){var c=a.split(" ");return d.utils.each(c,function(a){this.element.addEventListener(a,b,!1)},this),this},off:function(a,b){var c=a.split(" ");return d.utils.each(c,function(a){this.element.removeEventListener(a,b,!1)},this),this},trigger:function(a,b){b||(b={});var c=d.DOCUMENT.createEvent("Event");c.initEvent(a,!0,!0),c.gesture=b;var e=this.element;return d.utils.hasParent(b.target,e)&&(e=b.target),e.dispatchEvent(c),this},enable:function(a){return this.enabled=a,this}};var e=null,f=!1,g=!1;d.event={bindDom:function(a,b,c){var e=b.split(" ");d.utils.each(e,function(b){a.addEventListener(b,c,!1)})},onTouch:function(a,b,c){var h=this;this.bindDom(a,d.EVENT_TYPES[b],function(i){var j=i.type.toLowerCase();if(!j.match(/mouse/)||!g){j.match(/touch/)||j.match(/pointerdown/)||j.match(/mouse/)&&1===i.which?f=!0:j.match(/mouse/)&&!i.which&&(f=!1),j.match(/touch|pointer/)&&(g=!0);var k=0;f&&(d.HAS_POINTEREVENTS&&b!=d.EVENT_END?k=d.PointerEvent.updatePointer(b,i):j.match(/touch/)?k=i.touches.length:g||(k=j.match(/up/)?0:1),k>0&&b==d.EVENT_END?b=d.EVENT_MOVE:k||(b=d.EVENT_END),(k||null===e)&&(e=i),c.call(d.detection,h.collectEventData(a,b,h.getTouchList(e,b),i)),d.HAS_POINTEREVENTS&&b==d.EVENT_END&&(k=d.PointerEvent.updatePointer(b,i))),k||(e=null,f=!1,g=!1,d.PointerEvent.reset())}})},determineEventTypes:function(){var a;a=d.HAS_POINTEREVENTS?d.PointerEvent.getEvents():d.NO_MOUSEEVENTS?["touchstart","touchmove","touchend touchcancel"]:["touchstart mousedown","touchmove mousemove","touchend touchcancel mouseup"],d.EVENT_TYPES[d.EVENT_START]=a[0],d.EVENT_TYPES[d.EVENT_MOVE]=a[1],d.EVENT_TYPES[d.EVENT_END]=a[2]},getTouchList:function(a){return d.HAS_POINTEREVENTS?d.PointerEvent.getTouchList():a.touches?a.touches:(a.identifier=1,[a])},collectEventData:function(a,b,c,e){var f=d.POINTER_TOUCH;return(e.type.match(/mouse/)||d.PointerEvent.matchType(d.POINTER_MOUSE,e))&&(f=d.POINTER_MOUSE),{center:d.utils.getCenter(c),timeStamp:(new Date).getTime(),target:e.target,touches:c,eventType:b,pointerType:f,srcEvent:e,preventDefault:function(){this.srcEvent.preventManipulation&&this.srcEvent.preventManipulation(),this.srcEvent.preventDefault&&this.srcEvent.preventDefault()},stopPropagation:function(){this.srcEvent.stopPropagation()},stopDetect:function(){return d.detection.stopDetect()}}}},d.PointerEvent={pointers:{},getTouchList:function(){var a=this,b=[];return d.utils.each(a.pointers,function(a){b.push(a)}),b},updatePointer:function(a,b){return a==d.EVENT_END?this.pointers={}:(b.identifier=b.pointerId,this.pointers[b.pointerId]=b),Object.keys(this.pointers).length},matchType:function(a,b){if(!b.pointerType)return!1;var c=b.pointerType,e={};return e[d.POINTER_MOUSE]=c===b.MSPOINTER_TYPE_MOUSE||c===d.POINTER_MOUSE,e[d.POINTER_TOUCH]=c===b.MSPOINTER_TYPE_TOUCH||c===d.POINTER_TOUCH,e[d.POINTER_PEN]=c===b.MSPOINTER_TYPE_PEN||c===d.POINTER_PEN,e[a]},getEvents:function(){return["pointerdown MSPointerDown","pointermove MSPointerMove","pointerup pointercancel MSPointerUp MSPointerCancel"]},reset:function(){this.pointers={}}},d.detection={gestures:[],current:null,previous:null,stopped:!1,startDetect:function(a,b){this.current||(this.stopped=!1,this.current={inst:a,startEvent:d.utils.extend({},b),lastEvent:!1,name:""},this.detect(b))},detect:function(a){if(this.current&&!this.stopped){a=this.extendEventData(a);var b=this.current.inst.options;return d.utils.each(this.gestures,function(c){return this.stopped||b[c.name]===!1||c.handler.call(c,a,this.current.inst)!==!1?void 0:(this.stopDetect(),!1)},this),this.current&&(this.current.lastEvent=a),a.eventType==d.EVENT_END&&!a.touches.length-1&&this.stopDetect(),a}},stopDetect:function(){this.previous=d.utils.extend({},this.current),this.current=null,this.stopped=!0},extendEventData:function(a){var b=this.current.startEvent;!b||a.touches.length==b.touches.length&&a.touches!==b.touches||(b.touches=[],d.utils.each(a.touches,function(a){b.touches.push(d.utils.extend({},a))}));var c,e,f=a.timeStamp-b.timeStamp,g=a.center.pageX-b.center.pageX,h=a.center.pageY-b.center.pageY,i=d.utils.getVelocity(f,g,h);return"end"===a.eventType?(c=this.current.lastEvent&&this.current.lastEvent.interimAngle,e=this.current.lastEvent&&this.current.lastEvent.interimDirection):(c=this.current.lastEvent&&d.utils.getAngle(this.current.lastEvent.center,a.center),e=this.current.lastEvent&&d.utils.getDirection(this.current.lastEvent.center,a.center)),d.utils.extend(a,{deltaTime:f,deltaX:g,deltaY:h,velocityX:i.x,velocityY:i.y,distance:d.utils.getDistance(b.center,a.center),angle:d.utils.getAngle(b.center,a.center),interimAngle:c,direction:d.utils.getDirection(b.center,a.center),interimDirection:e,scale:d.utils.getScale(b.touches,a.touches),rotation:d.utils.getRotation(b.touches,a.touches),startEvent:b}),a},register:function(a){var c=a.defaults||{};return c[a.name]===b&&(c[a.name]=!0),d.utils.extend(d.defaults,c,!0),a.index=a.index||1e3,this.gestures.push(a),this.gestures.sort(function(a,b){return a.indexb.index?1:0}),this.gestures}},d.gestures.Drag={name:"drag",index:50,defaults:{drag_min_distance:10,correct_for_drag_min_distance:!0,drag_max_touches:1,drag_block_horizontal:!1,drag_block_vertical:!1,drag_lock_to_axis:!1,drag_lock_min_distance:25},triggered:!1,handler:function(a,b){if(d.detection.current.name!=this.name&&this.triggered)return b.trigger(this.name+"end",a),this.triggered=!1,void 0;if(!(b.options.drag_max_touches>0&&a.touches.length>b.options.drag_max_touches))switch(a.eventType){case d.EVENT_START:this.triggered=!1;break;case d.EVENT_MOVE:if(a.distance0)){var c=Math.abs(b.options.drag_min_distance/a.distance);d.detection.current.startEvent.center.pageX+=a.deltaX*c,d.detection.current.startEvent.center.pageY+=a.deltaY*c,a=d.detection.extendEventData(a)}(d.detection.current.lastEvent.drag_locked_to_axis||b.options.drag_lock_to_axis&&b.options.drag_lock_min_distance<=a.distance)&&(a.drag_locked_to_axis=!0);var e=d.detection.current.lastEvent.direction;a.drag_locked_to_axis&&e!==a.direction&&(a.direction=d.utils.isVertical(e)?a.deltaY<0?d.DIRECTION_UP:d.DIRECTION_DOWN:a.deltaX<0?d.DIRECTION_LEFT:d.DIRECTION_RIGHT),this.triggered||(b.trigger(this.name+"start",a),this.triggered=!0),b.trigger(this.name,a),b.trigger(this.name+a.direction,a),(b.options.drag_block_vertical&&d.utils.isVertical(a.direction)||b.options.drag_block_horizontal&&!d.utils.isVertical(a.direction))&&a.preventDefault();break;case d.EVENT_END:this.triggered&&b.trigger(this.name+"end",a),this.triggered=!1}}},d.gestures.Hold={name:"hold",index:10,defaults:{hold_timeout:500,hold_threshold:1},timer:null,handler:function(a,b){switch(a.eventType){case d.EVENT_START:clearTimeout(this.timer),d.detection.current.name=this.name,this.timer=setTimeout(function(){"hold"==d.detection.current.name&&b.trigger("hold",a)},b.options.hold_timeout);break;case d.EVENT_MOVE:a.distance>b.options.hold_threshold&&clearTimeout(this.timer);break;case d.EVENT_END:clearTimeout(this.timer)}}},d.gestures.Release={name:"release",index:1/0,handler:function(a,b){a.eventType==d.EVENT_END&&b.trigger(this.name,a)}},d.gestures.Swipe={name:"swipe",index:40,defaults:{swipe_min_touches:1,swipe_max_touches:1,swipe_velocity:.7},handler:function(a,b){if(a.eventType==d.EVENT_END){if(b.options.swipe_max_touches>0&&a.touches.lengthb.options.swipe_max_touches)return;(a.velocityX>b.options.swipe_velocity||a.velocityY>b.options.swipe_velocity)&&(b.trigger(this.name,a),b.trigger(this.name+a.direction,a))}}},d.gestures.Tap={name:"tap",index:100,defaults:{tap_max_touchtime:250,tap_max_distance:10,tap_always:!0,doubletap_distance:20,doubletap_interval:300},handler:function(a,b){if(a.eventType==d.EVENT_END&&"touchcancel"!=a.srcEvent.type){var c=d.detection.previous,e=!1;if(a.deltaTime>b.options.tap_max_touchtime||a.distance>b.options.tap_max_distance)return;c&&"tap"==c.name&&a.timeStamp-c.lastEvent.timeStampb.options.transform_min_rotation&&b.trigger("rotate",a),c>b.options.transform_min_scale&&(b.trigger("pinch",a),b.trigger("pinch"+(a.scale<1?"in":"out"),a));break;case d.EVENT_END:this.triggered&&b.trigger(this.name+"end",a),this.triggered=!1}}},"function"==typeof define&&"object"==typeof define.amd&&define.amd?define(function(){return d}):"object"==typeof module&&"object"==typeof module.exports?module.exports=d:a.Hammer=d}(this); 9 | //# sourceMappingURL=hammer.min.map -------------------------------------------------------------------------------- /public/style/fonts/ClearSans-Bold-webfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | --------------------------------------------------------------------------------