Endless Game. Allow user to continue the game.
Endless Game. Allow user to continue the game.

- main.css isn't compiled
- a special class `.tile-gold` is added to tiles above 2048

file:a/index.html -> file:b/index.html
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>2048</title> <title>2048</title>
   
<link href="style/main.css" rel="stylesheet" type="text/css"> <link href="style/main.css" rel="stylesheet" type="text/css">
<link rel="shortcut icon" href="favicon.ico"> <link rel="shortcut icon" href="favicon.ico">
<link rel="apple-touch-icon" href="meta/apple-touch-icon.png"> <link rel="apple-touch-icon" href="meta/apple-touch-icon.png">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
   
<meta name="HandheldFriendly" content="True"> <meta name="HandheldFriendly" content="True">
<meta name="MobileOptimized" content="320"> <meta name="MobileOptimized" content="320">
<meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0, maximum-scale=1, user-scalable=no"> <meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0, maximum-scale=1, user-scalable=no">
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="heading"> <div class="heading">
<h1 class="title">2048</h1> <h1 class="title">2048</h1>
<div class="scores-container"> <div class="scores-container">
<div class="score-container">0</div> <div class="score-container">0</div>
<div class="best-container">0</div> <div class="best-container">0</div>
</div> </div>
</div> </div>
<p class="game-intro">Join the numbers and get to the <strong>2048 tile!</strong></p> <p class="game-intro">Join the numbers and get to the <strong>2048 tile!</strong></p>
   
<div class="game-container"> <div class="game-container">
<div class="game-message"> <div class="game-message">
<p></p> <p></p>
<div class="lower"> <div class="lower">
  <a class="keep-playing-button">Keep playing</a>
<a class="retry-button">Try again</a> <a class="retry-button">Try again</a>
</div> </div>
</div> </div>
   
<div class="grid-container"> <div class="grid-container">
<div class="grid-row"> <div class="grid-row">
<div class="grid-cell"></div> <div class="grid-cell"></div>
<div class="grid-cell"></div> <div class="grid-cell"></div>
<div class="grid-cell"></div> <div class="grid-cell"></div>
<div class="grid-cell"></div> <div class="grid-cell"></div>
</div> </div>
<div class="grid-row"> <div class="grid-row">
<div class="grid-cell"></div> <div class="grid-cell"></div>
<div class="grid-cell"></div> <div class="grid-cell"></div>
<div class="grid-cell"></div> <div class="grid-cell"></div>
<div class="grid-cell"></div> <div class="grid-cell"></div>
</div> </div>
<div class="grid-row"> <div class="grid-row">
<div class="grid-cell"></div> <div class="grid-cell"></div>
<div class="grid-cell"></div> <div class="grid-cell"></div>
<div class="grid-cell"></div> <div class="grid-cell"></div>
<div class="grid-cell"></div> <div class="grid-cell"></div>
</div> </div>
<div class="grid-row"> <div class="grid-row">
<div class="grid-cell"></div> <div class="grid-cell"></div>
<div class="grid-cell"></div> <div class="grid-cell"></div>
<div class="grid-cell"></div> <div class="grid-cell"></div>
<div class="grid-cell"></div> <div class="grid-cell"></div>
</div> </div>
</div> </div>
   
<div class="tile-container"> <div class="tile-container">
   
</div> </div>
</div> </div>
   
<p class="game-explanation"> <p class="game-explanation">
<strong class="important">How to play:</strong> Use your <strong>arrow keys</strong> to move the tiles. When two tiles with the same number touch, they <strong>merge into one!</strong> <strong class="important">How to play:</strong> Use your <strong>arrow keys</strong> to move the tiles. When two tiles with the same number touch, they <strong>merge into one!</strong>
</p> </p>
<hr> <hr>
<p> <p>
Created by <a href="http://gabrielecirulli.com" target="_blank">Gabriele Cirulli.</a> Based on <a href="https://itunes.apple.com/us/app/1024!/id823499224" target="_blank">1024 by Veewo Studio</a> and conceptually similar to <a href="http://asherv.com/threes/" target="_blank">Threes by Asher Vollmer.</a> Created by <a href="http://gabrielecirulli.com" target="_blank">Gabriele Cirulli.</a> Based on <a href="https://itunes.apple.com/us/app/1024!/id823499224" target="_blank">1024 by Veewo Studio</a> and conceptually similar to <a href="http://asherv.com/threes/" target="_blank">Threes by Asher Vollmer.</a>
</p> </p>
</div> </div>
   
<script src="js/animframe_polyfill.js"></script> <script src="js/animframe_polyfill.js"></script>
<script src="js/keyboard_input_manager.js"></script> <script src="js/keyboard_input_manager.js"></script>
<script src="js/html_actuator.js"></script> <script src="js/html_actuator.js"></script>
<script src="js/grid.js"></script> <script src="js/grid.js"></script>
<script src="js/tile.js"></script> <script src="js/tile.js"></script>
<script src="js/local_score_manager.js"></script> <script src="js/local_score_manager.js"></script>
<script src="js/game_manager.js"></script> <script src="js/game_manager.js"></script>
<script src="js/application.js"></script> <script src="js/application.js"></script>
</body> </body>
</html> </html>
   
function GameManager(size, InputManager, Actuator, ScoreManager) { function GameManager(size, InputManager, Actuator, ScoreManager) {
this.size = size; // Size of the grid this.size = size; // Size of the grid
this.inputManager = new InputManager; this.inputManager = new InputManager;
this.scoreManager = new ScoreManager; this.scoreManager = new ScoreManager;
this.actuator = new Actuator; this.actuator = new Actuator;
   
this.startTiles = 2; this.startTiles = 2;
   
this.inputManager.on("move", this.move.bind(this)); this.inputManager.on("move", this.move.bind(this));
this.inputManager.on("restart", this.restart.bind(this)); this.inputManager.on("restart", this.restart.bind(this));
  this.inputManager.on("keepPlaying", this.keepPlaying.bind(this));
   
this.setup(); this.setup();
} }
   
// Restart the game // Restart the game
GameManager.prototype.restart = function () { GameManager.prototype.restart = function () {
this.actuator.restart(); this.actuator.restart();
this.setup(); this.setup();
}; };
   
  // Keep playing after winning
  GameManager.prototype.keepPlaying = function () {
  this.keepPlaying = true;
  this.actuator.keepPlaying();
  };
   
  GameManager.prototype.isGameOver = function() {
  if (this.over || (this.won && !this.keepPlaying) ) {
  return true;
  }
  else {
  return false;
  }
  }
   
   
// Set up the game // Set up the game
GameManager.prototype.setup = function () { GameManager.prototype.setup = function () {
this.grid = new Grid(this.size); this.grid = new Grid(this.size);
   
this.score = 0; this.score = 0;
this.over = false; this.over = false;
this.won = false; this.won = false;
  this.keepPlaying = false;
   
// Add the initial tiles // Add the initial tiles
this.addStartTiles(); this.addStartTiles();
   
// Update the actuator // Update the actuator
this.actuate(); this.actuate();
}; };
   
// Set up the initial tiles to start the game with // Set up the initial tiles to start the game with
GameManager.prototype.addStartTiles = function () { GameManager.prototype.addStartTiles = function () {
for (var i = 0; i < this.startTiles; i++) { for (var i = 0; i < this.startTiles; i++) {
this.addRandomTile(); this.addRandomTile();
} }
}; };
   
// Adds a tile in a random position // Adds a tile in a random position
GameManager.prototype.addRandomTile = function () { GameManager.prototype.addRandomTile = function () {
if (this.grid.cellsAvailable()) { if (this.grid.cellsAvailable()) {
var value = Math.random() < 0.9 ? 2 : 4; var value = Math.random() < 0.9 ? 2 : 4;
var tile = new Tile(this.grid.randomAvailableCell(), value); var tile = new Tile(this.grid.randomAvailableCell(), value);
   
this.grid.insertTile(tile); this.grid.insertTile(tile);
} }
}; };
   
// Sends the updated grid to the actuator // Sends the updated grid to the actuator
GameManager.prototype.actuate = function () { GameManager.prototype.actuate = function () {
if (this.scoreManager.get() < this.score) { if (this.scoreManager.get() < this.score) {
this.scoreManager.set(this.score); this.scoreManager.set(this.score);
} }
   
this.actuator.actuate(this.grid, { this.actuator.actuate(this.grid, {
score: this.score, score: this.score,
over: this.over, over: this.over,
won: this.won, won: this.won,
bestScore: this.scoreManager.get() bestScore: this.scoreManager.get(),
  gameOver: this.isGameOver()
}); });
   
}; };
   
// Save all tile positions and remove merger info // Save all tile positions and remove merger info
GameManager.prototype.prepareTiles = function () { GameManager.prototype.prepareTiles = function () {
this.grid.eachCell(function (x, y, tile) { this.grid.eachCell(function (x, y, tile) {
if (tile) { if (tile) {
tile.mergedFrom = null; tile.mergedFrom = null;
tile.savePosition(); tile.savePosition();
} }
}); });
}; };
   
// Move a tile and its representation // Move a tile and its representation
GameManager.prototype.moveTile = function (tile, cell) { GameManager.prototype.moveTile = function (tile, cell) {
this.grid.cells[tile.x][tile.y] = null; this.grid.cells[tile.x][tile.y] = null;
this.grid.cells[cell.x][cell.y] = tile; this.grid.cells[cell.x][cell.y] = tile;
tile.updatePosition(cell); tile.updatePosition(cell);
}; };
   
// Move tiles on the grid in the specified direction // Move tiles on the grid in the specified direction
GameManager.prototype.move = function (direction) { GameManager.prototype.move = function (direction) {
// 0: up, 1: right, 2:down, 3: left // 0: up, 1: right, 2:down, 3: left
var self = this; var self = this;
   
if (this.over || this.won) return; // Don't do anything if the game's over if (this.isGameOver()) return; // Don't do anything if the game's over
   
var cell, tile; var cell, tile;
   
var vector = this.getVector(direction); var vector = this.getVector(direction);
var traversals = this.buildTraversals(vector); var traversals = this.buildTraversals(vector);
var moved = false; var moved = false;
   
// Save the current tile positions and remove merger information // Save the current tile positions and remove merger information
this.prepareTiles(); this.prepareTiles();
   
// Traverse the grid in the right direction and move tiles // Traverse the grid in the right direction and move tiles
traversals.x.forEach(function (x) { traversals.x.forEach(function (x) {
traversals.y.forEach(function (y) { traversals.y.forEach(function (y) {
cell = { x: x, y: y }; cell = { x: x, y: y };
tile = self.grid.cellContent(cell); tile = self.grid.cellContent(cell);
   
if (tile) { if (tile) {
var positions = self.findFarthestPosition(cell, vector); var positions = self.findFarthestPosition(cell, vector);
var next = self.grid.cellContent(positions.next); var next = self.grid.cellContent(positions.next);
   
// Only one merger per row traversal? // Only one merger per row traversal?
if (next && next.value === tile.value && !next.mergedFrom) { if (next && next.value === tile.value && !next.mergedFrom) {
var merged = new Tile(positions.next, tile.value * 2); var merged = new Tile(positions.next, tile.value * 2);
merged.mergedFrom = [tile, next]; merged.mergedFrom = [tile, next];
   
self.grid.insertTile(merged); self.grid.insertTile(merged);
self.grid.removeTile(tile); self.grid.removeTile(tile);
   
// Converge the two tiles' positions // Converge the two tiles' positions
tile.updatePosition(positions.next); tile.updatePosition(positions.next);
   
// Update the score // Update the score
self.score += merged.value; self.score += merged.value;
   
// The mighty 2048 tile // The mighty 2048 tile
if (merged.value === 2048) self.won = true; if (merged.value === 2048) self.won = true;
} else { } else {
self.moveTile(tile, positions.farthest); self.moveTile(tile, positions.farthest);
} }
   
if (!self.positionsEqual(cell, tile)) { if (!self.positionsEqual(cell, tile)) {
moved = true; // The tile moved from its original cell! moved = true; // The tile moved from its original cell!
} }
} }
}); });
}); });
   
if (moved) { if (moved) {
this.addRandomTile(); this.addRandomTile();
   
if (!this.movesAvailable()) { if (!this.movesAvailable()) {
this.over = true; // Game over! this.over = true; // Game over!
} }
   
this.actuate(); this.actuate();
} }
}; };
   
// Get the vector representing the chosen direction // Get the vector representing the chosen direction
GameManager.prototype.getVector = function (direction) { GameManager.prototype.getVector = function (direction) {
// Vectors representing tile movement // Vectors representing tile movement
var map = { var map = {
0: { x: 0, y: -1 }, // up 0: { x: 0, y: -1 }, // up
1: { x: 1, y: 0 }, // right 1: { x: 1, y: 0 }, // right
2: { x: 0, y: 1 }, // down 2: { x: 0, y: 1 }, // down
3: { x: -1, y: 0 } // left 3: { x: -1, y: 0 } // left
}; };
   
return map[direction]; return map[direction];
}; };
   
// Build a list of positions to traverse in the right order // Build a list of positions to traverse in the right order
GameManager.prototype.buildTraversals = function (vector) { GameManager.prototype.buildTraversals = function (vector) {
var traversals = { x: [], y: [] }; var traversals = { x: [], y: [] };
   
for (var pos = 0; pos < this.size; pos++) { for (var pos = 0; pos < this.size; pos++) {
traversals.x.push(pos); traversals.x.push(pos);
traversals.y.push(pos); traversals.y.push(pos);
} }
   
// Always traverse from the farthest cell in the chosen direction // Always traverse from the farthest cell in the chosen direction
if (vector.x === 1) traversals.x = traversals.x.reverse(); if (vector.x === 1) traversals.x = traversals.x.reverse();
if (vector.y === 1) traversals.y = traversals.y.reverse(); if (vector.y === 1) traversals.y = traversals.y.reverse();
   
return traversals; return traversals;
}; };
   
GameManager.prototype.findFarthestPosition = function (cell, vector) { GameManager.prototype.findFarthestPosition = function (cell, vector) {
var previous; var previous;
   
// Progress towards the vector direction until an obstacle is found // Progress towards the vector direction until an obstacle is found
do { do {
previous = cell; previous = cell;
cell = { x: previous.x + vector.x, y: previous.y + vector.y }; cell = { x: previous.x + vector.x, y: previous.y + vector.y };
} while (this.grid.withinBounds(cell) && } while (this.grid.withinBounds(cell) &&
this.grid.cellAvailable(cell)); this.grid.cellAvailable(cell));
   
return { return {
farthest: previous, farthest: previous,
next: cell // Used to check if a merge is required next: cell // Used to check if a merge is required
}; };
}; };
   
GameManager.prototype.movesAvailable = function () { GameManager.prototype.movesAvailable = function () {
return this.grid.cellsAvailable() || this.tileMatchesAvailable(); return this.grid.cellsAvailable() || this.tileMatchesAvailable();
}; };
   
// Check for available matches between tiles (more expensive check) // Check for available matches between tiles (more expensive check)
GameManager.prototype.tileMatchesAvailable = function () { GameManager.prototype.tileMatchesAvailable = function () {
var self = this; var self = this;
   
var tile; var tile;
   
for (var x = 0; x < this.size; x++) { for (var x = 0; x < this.size; x++) {
for (var y = 0; y < this.size; y++) { for (var y = 0; y < this.size; y++) {
tile = this.grid.cellContent({ x: x, y: y }); tile = this.grid.cellContent({ x: x, y: y });
   
if (tile) { if (tile) {
for (var direction = 0; direction < 4; direction++) { for (var direction = 0; direction < 4; direction++) {
var vector = self.getVector(direction); var vector = self.getVector(direction);
var cell = { x: x + vector.x, y: y + vector.y }; var cell = { x: x + vector.x, y: y + vector.y };
   
var other = self.grid.cellContent(cell); var other = self.grid.cellContent(cell);
   
if (other && other.value === tile.value) { if (other && other.value === tile.value) {
return true; // These two tiles can be merged return true; // These two tiles can be merged
} }
} }
} }
} }
} }
   
return false; return false;
}; };
   
GameManager.prototype.positionsEqual = function (first, second) { GameManager.prototype.positionsEqual = function (first, second) {
return first.x === second.x && first.y === second.y; return first.x === second.x && first.y === second.y;
}; };
   
function HTMLActuator() { function HTMLActuator() {
this.tileContainer = document.querySelector(".tile-container"); this.tileContainer = document.querySelector(".tile-container");
this.scoreContainer = document.querySelector(".score-container"); this.scoreContainer = document.querySelector(".score-container");
this.bestContainer = document.querySelector(".best-container"); this.bestContainer = document.querySelector(".best-container");
this.messageContainer = document.querySelector(".game-message"); this.messageContainer = document.querySelector(".game-message");
   
this.score = 0; this.score = 0;
} }
   
HTMLActuator.prototype.actuate = function (grid, metadata) { HTMLActuator.prototype.actuate = function (grid, metadata) {
var self = this; var self = this;
   
window.requestAnimationFrame(function () { window.requestAnimationFrame(function () {
self.clearContainer(self.tileContainer); self.clearContainer(self.tileContainer);
   
grid.cells.forEach(function (column) { grid.cells.forEach(function (column) {
column.forEach(function (cell) { column.forEach(function (cell) {
if (cell) { if (cell) {
self.addTile(cell); self.addTile(cell);
} }
}); });
}); });
   
self.updateScore(metadata.score); self.updateScore(metadata.score);
self.updateBestScore(metadata.bestScore); self.updateBestScore(metadata.bestScore);
   
if (metadata.over) self.message(false); // You lose if (metadata.gameOver) {
if (metadata.won) self.message(true); // You win! if (metadata.over) self.message(false); // You lose
  else if (metadata.won) self.message(true); // You win!
  }
   
}); });
}; };
   
HTMLActuator.prototype.restart = function () { HTMLActuator.prototype.restart = function () {
this.clearMessage(); this.clearMessage();
}; };
   
  HTMLActuator.prototype.keepPlaying = function () {
  this.clearMessage();
  }
   
HTMLActuator.prototype.clearContainer = function (container) { HTMLActuator.prototype.clearContainer = function (container) {
while (container.firstChild) { while (container.firstChild) {
container.removeChild(container.firstChild); container.removeChild(container.firstChild);
} }
}; };
   
HTMLActuator.prototype.addTile = function (tile) { HTMLActuator.prototype.addTile = function (tile) {
var self = this; var self = this;
   
var element = document.createElement("div"); var element = document.createElement("div");
var position = tile.previousPosition || { x: tile.x, y: tile.y }; var position = tile.previousPosition || { x: tile.x, y: tile.y };
positionClass = this.positionClass(position); var positionClass = this.positionClass(position);
  var styleClass = this.styleClass(tile);
   
// We can't use classlist because it somehow glitches when replacing classes // We can't use classlist because it somehow glitches when replacing classes
var classes = ["tile", "tile-" + tile.value, positionClass]; var classes = ["tile", styleClass, positionClass];
this.applyClasses(element, classes); this.applyClasses(element, classes);
   
element.textContent = tile.value; element.textContent = tile.value;
   
if (tile.previousPosition) { if (tile.previousPosition) {
// Make sure that the tile gets rendered in the previous position first // Make sure that the tile gets rendered in the previous position first
window.requestAnimationFrame(function () { window.requestAnimationFrame(function () {
classes[2] = self.positionClass({ x: tile.x, y: tile.y }); classes[2] = self.positionClass({ x: tile.x, y: tile.y });
self.applyClasses(element, classes); // Update the position self.applyClasses(element, classes); // Update the position
}); });
} else if (tile.mergedFrom) { } else if (tile.mergedFrom) {
classes.push("tile-merged"); classes.push("tile-merged");
this.applyClasses(element, classes); this.applyClasses(element, classes);
   
// Render the tiles that merged // Render the tiles that merged
tile.mergedFrom.forEach(function (merged) { tile.mergedFrom.forEach(function (merged) {
self.addTile(merged); self.addTile(merged);
}); });
} else { } else {
classes.push("tile-new"); classes.push("tile-new");
this.applyClasses(element, classes); this.applyClasses(element, classes);
} }
   
// Put the tile on the board // Put the tile on the board
this.tileContainer.appendChild(element); this.tileContainer.appendChild(element);
}; };
   
HTMLActuator.prototype.applyClasses = function (element, classes) { HTMLActuator.prototype.applyClasses = function (element, classes) {
element.setAttribute("class", classes.join(" ")); element.setAttribute("class", classes.join(" "));
}; };
   
HTMLActuator.prototype.normalizePosition = function (position) { HTMLActuator.prototype.normalizePosition = function (position) {
return { x: position.x + 1, y: position.y + 1 }; return { x: position.x + 1, y: position.y + 1 };
}; };
   
HTMLActuator.prototype.positionClass = function (position) { HTMLActuator.prototype.positionClass = function (position) {
position = this.normalizePosition(position); position = this.normalizePosition(position);
return "tile-position-" + position.x + "-" + position.y; return "tile-position-" + position.x + "-" + position.y;
}; };
   
  HTMLActuator.prototype.styleClass = function (tile) {
  var className = 'tile-'+tile.value;
   
  if (tile.value > 2048 ) {
  className = className+' tile-gold';
  }
  return className;
  };
   
HTMLActuator.prototype.updateScore = function (score) { HTMLActuator.prototype.updateScore = function (score) {
this.clearContainer(this.scoreContainer); this.clearContainer(this.scoreContainer);
   
var difference = score - this.score; var difference = score - this.score;
this.score = score; this.score = score;
   
this.scoreContainer.textContent = this.score; this.scoreContainer.textContent = this.score;
   
if (difference > 0) { if (difference > 0) {
var addition = document.createElement("div"); var addition = document.createElement("div");
addition.classList.add("score-addition"); addition.classList.add("score-addition");
addition.textContent = "+" + difference; addition.textContent = "+" + difference;
   
this.scoreContainer.appendChild(addition); this.scoreContainer.appendChild(addition);
} }
}; };
   
HTMLActuator.prototype.updateBestScore = function (bestScore) { HTMLActuator.prototype.updateBestScore = function (bestScore) {
this.bestContainer.textContent = bestScore; this.bestContainer.textContent = bestScore;
}; };
   
HTMLActuator.prototype.message = function (won) { HTMLActuator.prototype.message = function (won) {
var type = won ? "game-won" : "game-over"; var type = won ? "game-won" : "game-over";
var message = won ? "You win!" : "Game over!"; var message = won ? "You win!" : "Game over!";
   
this.messageContainer.classList.add(type); this.messageContainer.classList.add(type);
this.messageContainer.getElementsByTagName("p")[0].textContent = message; this.messageContainer.getElementsByTagName("p")[0].textContent = message;
}; };
   
HTMLActuator.prototype.clearMessage = function () { HTMLActuator.prototype.clearMessage = function () {
this.messageContainer.classList.remove("game-won", "game-over"); this.messageContainer.classList.remove("game-won", "game-over");
}; };
   
function KeyboardInputManager() { function KeyboardInputManager() {
this.events = {}; this.events = {};
   
this.listen(); this.listen();
} }
   
KeyboardInputManager.prototype.on = function (event, callback) { KeyboardInputManager.prototype.on = function (event, callback) {
if (!this.events[event]) { if (!this.events[event]) {
this.events[event] = []; this.events[event] = [];
} }
this.events[event].push(callback); this.events[event].push(callback);
}; };
   
KeyboardInputManager.prototype.emit = function (event, data) { KeyboardInputManager.prototype.emit = function (event, data) {
var callbacks = this.events[event]; var callbacks = this.events[event];
if (callbacks) { if (callbacks) {
callbacks.forEach(function (callback) { callbacks.forEach(function (callback) {
callback(data); callback(data);
}); });
} }
}; };
   
KeyboardInputManager.prototype.listen = function () { KeyboardInputManager.prototype.listen = function () {
var self = this; var self = this;
   
var map = { var map = {
38: 0, // Up 38: 0, // Up
39: 1, // Right 39: 1, // Right
40: 2, // Down 40: 2, // Down
37: 3, // Left 37: 3, // Left
75: 0, // vim keybindings 75: 0, // vim keybindings
76: 1, 76: 1,
74: 2, 74: 2,
72: 3, 72: 3,
87: 0, // W 87: 0, // W
68: 1, // D 68: 1, // D
83: 2, // S 83: 2, // S
65: 3 // A 65: 3 // A
}; };
   
document.addEventListener("keydown", function (event) { document.addEventListener("keydown", function (event) {
var modifiers = event.altKey || event.ctrlKey || event.metaKey || var modifiers = event.altKey || event.ctrlKey || event.metaKey ||
event.shiftKey; event.shiftKey;
var mapped = map[event.which]; var mapped = map[event.which];
   
if (!modifiers) { if (!modifiers) {
if (mapped !== undefined) { if (mapped !== undefined) {
event.preventDefault(); event.preventDefault();
self.emit("move", mapped); self.emit("move", mapped);
} }
   
if (event.which === 32) self.restart.bind(self)(event); if (event.which === 32) self.restart.bind(self)(event);
} }
}); });
   
var retry = document.getElementsByClassName("retry-button")[0]; var retry = document.querySelector(".retry-button");
retry.addEventListener("click", this.restart.bind(this)); retry.addEventListener("click", this.restart.bind(this));
retry.addEventListener("touchend", this.restart.bind(this)); retry.addEventListener("touchend", this.restart.bind(this));
   
  var keepPlaying = document.querySelector(".keep-playing-button");
  keepPlaying.addEventListener("click", this.keepPlaying.bind(this));
   
// Listen to swipe events // Listen to swipe events
var touchStartClientX, touchStartClientY; var touchStartClientX, touchStartClientY;
var gameContainer = document.getElementsByClassName("game-container")[0]; var gameContainer = document.getElementsByClassName("game-container")[0];
   
gameContainer.addEventListener("touchstart", function (event) { gameContainer.addEventListener("touchstart", function (event) {
if (event.touches.length > 1) return; if (event.touches.length > 1) return;
   
touchStartClientX = event.touches[0].clientX; touchStartClientX = event.touches[0].clientX;
touchStartClientY = event.touches[0].clientY; touchStartClientY = event.touches[0].clientY;
event.preventDefault(); event.preventDefault();
}); });
   
gameContainer.addEventListener("touchmove", function (event) { gameContainer.addEventListener("touchmove", function (event) {
event.preventDefault(); event.preventDefault();
}); });
   
gameContainer.addEventListener("touchend", function (event) { gameContainer.addEventListener("touchend", function (event) {
if (event.touches.length > 0) return; if (event.touches.length > 0) return;
   
var dx = event.changedTouches[0].clientX - touchStartClientX; var dx = event.changedTouches[0].clientX - touchStartClientX;
var absDx = Math.abs(dx); var absDx = Math.abs(dx);
   
var dy = event.changedTouches[0].clientY - touchStartClientY; var dy = event.changedTouches[0].clientY - touchStartClientY;
var absDy = Math.abs(dy); var absDy = Math.abs(dy);
   
if (Math.max(absDx, absDy) > 10) { if (Math.max(absDx, absDy) > 10) {
// (right : left) : (down : up) // (right : left) : (down : up)
self.emit("move", absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0)); self.emit("move", absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0));
} }
}); });
}; };
   
KeyboardInputManager.prototype.restart = function (event) { KeyboardInputManager.prototype.restart = function (event) {
event.preventDefault(); event.preventDefault();
this.emit("restart"); this.emit("restart");
}; };
   
  KeyboardInputManager.prototype.keepPlaying = function (event) {
  event.preventDefault();
  this.emit("keepPlaying");
  };
   
@import "helpers"; @import "helpers";
@import "fonts/clear-sans.css"; @import "fonts/clear-sans.css";
   
$field-width: 500px; $field-width: 500px;
$grid-spacing: 15px; $grid-spacing: 15px;
$grid-row-cells: 4; $grid-row-cells: 4;
$tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells; $tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells;
$tile-border-radius: 3px; $tile-border-radius: 3px;
   
$text-color: #776E65; $text-color: #776E65;
$bright-text-color: #f9f6f2; $bright-text-color: #f9f6f2;
   
$tile-color: #eee4da; $tile-color: #eee4da;
$tile-gold-color: #edc22e; $tile-gold-color: #edc22e;
$tile-gold-glow-color: lighten($tile-gold-color, 15%); $tile-gold-glow-color: lighten($tile-gold-color, 15%);
   
$game-container-background: #bbada0; $game-container-background: #bbada0;
   
$transition-speed: 100ms; $transition-speed: 100ms;
   
html, body { html, body {
margin: 0; margin: 0;
padding: 0; padding: 0;
   
background: #faf8ef; background: #faf8ef;
color: $text-color; color: $text-color;
font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif; font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif;
font-size: 18px; font-size: 18px;
} }
   
body { body {
margin: 80px 0; margin: 80px 0;
} }
   
.heading:after { .heading:after {
content: ""; content: "";
display: block; display: block;
clear: both; clear: both;
} }
   
h1.title { h1.title {
font-size: 80px; font-size: 80px;
font-weight: bold; font-weight: bold;
margin: 0; margin: 0;
display: block; display: block;
float: left; float: left;
} }
   
@include keyframes(move-up) { @include keyframes(move-up) {
0% { 0% {
top: 25px; top: 25px;
opacity: 1; opacity: 1;
} }
   
100% { 100% {
top: -50px; top: -50px;
opacity: 0; opacity: 0;
} }
} }
   
.scores-container { .scores-container {
float: right; float: right;
text-align: right; text-align: right;
} }
   
.score-container, .best-container { .score-container, .best-container {
$height: 25px; $height: 25px;
   
position: relative; position: relative;
display: inline-block; display: inline-block;
background: $game-container-background; background: $game-container-background;
padding: 15px 25px; padding: 15px 25px;
font-size: $height; font-size: $height;
height: $height; height: $height;
line-height: $height + 22px; line-height: $height + 22px;
font-weight: bold; font-weight: bold;
border-radius: 3px; border-radius: 3px;
color: white; color: white;
margin-top: 8px; margin-top: 8px;
text-align: center; text-align: center;
   
&:after { &:after {
position: absolute; position: absolute;
width: 100%; width: 100%;
top: 10px; top: 10px;
left: 0; left: 0;
text-transform: uppercase; text-transform: uppercase;
font-size: 13px; font-size: 13px;
line-height: 13px; line-height: 13px;
text-align: center; text-align: center;
color: $tile-color; color: $tile-color;
} }
   
.score-addition { .score-addition {
position: absolute; position: absolute;
right: 30px; right: 30px;
color: red; color: red;
font-size: $height; font-size: $height;
line-height: $height; line-height: $height;
font-weight: bold; font-weight: bold;
color: rgba($text-color, .9); color: rgba($text-color, .9);
z-index: 100; z-index: 100;
@include animation(move-up 600ms ease-in); @include animation(move-up 600ms ease-in);
@include animation-fill-mode(both); @include animation-fill-mode(both);
} }
} }
   
.score-container:after { .score-container:after {
content: "Score"; content: "Score";
} }
   
.best-container:after { .best-container:after {
content: "Best" content: "Best"
} }
   
p { p {
margin-top: 0; margin-top: 0;
margin-bottom: 10px; margin-bottom: 10px;
line-height: 1.65; line-height: 1.65;
} }
   
a { a {
color: $text-color; color: $text-color;
font-weight: bold; font-weight: bold;
text-decoration: underline; text-decoration: underline;
cursor: pointer; cursor: pointer;
} }
   
strong { strong {
&.important { &.important {
text-transform: uppercase; text-transform: uppercase;
} }
} }
   
hr { hr {
border: none; border: none;
border-bottom: 1px solid lighten($text-color, 40%); border-bottom: 1px solid lighten($text-color, 40%);
margin-top: 20px; margin-top: 20px;
margin-bottom: 30px; margin-bottom: 30px;
} }
   
.container { .container {
width: $field-width; width: $field-width;
margin: 0 auto; margin: 0 auto;
} }
   
@include keyframes(fade-in) { @include keyframes(fade-in) {
0% { 0% {
opacity: 0; opacity: 0;
} }
   
100% { 100% {
opacity: 1; opacity: 1;
} }
} }
   
// Styles for buttons // Styles for buttons
@mixin button { @mixin button {
display: inline-block; display: inline-block;
background: darken($game-container-background, 20%); background: darken($game-container-background, 20%);
border-radius: 3px; border-radius: 3px;
padding: 0 20px; padding: 0 20px;
text-decoration: none; text-decoration: none;
color: $bright-text-color; color: $bright-text-color;
height: 40px; height: 40px;
line-height: 42px; line-height: 42px;
} }
   
// Game field mixin used to render CSS at different width // Game field mixin used to render CSS at different width
@mixin game-field { @mixin game-field {
.game-container { .game-container {
margin-top: 40px; margin-top: 40px;
position: relative; position: relative;
padding: $grid-spacing; padding: $grid-spacing;
   
cursor: default; cursor: default;
-webkit-touch-callout: none; -webkit-touch-callout: none;
-webkit-user-select: none; -webkit-user-select: none;
-moz-user-select: none; -moz-user-select: none;
   
background: $game-container-background; background: $game-container-background;
border-radius: $tile-border-radius * 2; border-radius: $tile-border-radius * 2;
width: $field-width; width: $field-width;
height: $field-width; height: $field-width;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
   
.game-message { .game-message {
display: none; display: none;
   
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
background: rgba($tile-color, .5); background: rgba($tile-color, .5);
z-index: 100; z-index: 100;
   
text-align: center; text-align: center;
   
p { p {
font-size: 60px; font-size: 60px;
font-weight: bold; font-weight: bold;
height: 60px; height: 60px;
line-height: 60px; line-height: 60px;
margin-top: 222px; margin-top: 222px;
// height: $field-width; // height: $field-width;
// line-height: $field-width; // line-height: $field-width;
} }
   
.lower { .lower {
display: block; display: block;
margin-top: 59px; margin-top: 59px;
} }
   
a { a {
@include button; @include button;
margin-left: 9px; margin-left: 9px;
// margin-top: 59px; // margin-top: 59px;
   
  &.keep-playing-button {
  display: none;
  }
} }
   
@include animation(fade-in 800ms ease $transition-speed * 12); @include animation(fade-in 800ms ease $transition-speed * 12);
@include animation-fill-mode(both); @include animation-fill-mode(both);
   
&.game-won { &.game-won {
background: rgba($tile-gold-color, .5); background: rgba($tile-gold-color, .5);
color: $bright-text-color; color: $bright-text-color;
   
  a.keep-playing-button {
  display: inline-block;
  }
} }
   
&.game-won, &.game-over { &.game-won, &.game-over {
display: block; display: block;
} }
} }
} }
   
.grid-container { .grid-container {
position: absolute; position: absolute;
z-index: 1; z-index: 1;
} }
   
.grid-row { .grid-row {
margin-bottom: $grid-spacing; margin-bottom: $grid-spacing;
   
&:last-child { &:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
   
&:after { &:after {
content: ""; content: "";
display: block; display: block;
clear: both; clear: both;
} }
} }
   
.grid-cell { .grid-cell {
width: $tile-size; width: $tile-size;
height: $tile-size; height: $tile-size;
margin-right: $grid-spacing; margin-right: $grid-spacing;
float: left; float: left;
   
border-radius: $tile-border-radius; border-radius: $tile-border-radius;
   
background: rgba($tile-color, .35); background: rgba($tile-color, .35);
   
&:last-child { &:last-child {
margin-right: 0; margin-right: 0;
} }
} }
   
.tile-container { .tile-container {
position: absolute; position: absolute;
z-index: 2; z-index: 2;
} }
   
.tile { .tile {
width: $tile-size; width: $tile-size;
height: $tile-size; height: $tile-size;
line-height: $tile-size + 10px; line-height: $tile-size + 10px;
   
// Build position classes // Build position classes
@for $x from 1 through $grid-row-cells { @for $x from 1 through $grid-row-cells {
@for $y from 1 through $grid-row-cells { @for $y from 1 through $grid-row-cells {
&.tile-position-#{$x}-#{$y} { &.tile-position-#{$x}-#{$y} {
position: absolute; position: absolute;
left: round(($tile-size + $grid-spacing) * ($x - 1)); left: round(($tile-size + $grid-spacing) * ($x - 1));
top: round(($tile-size + $grid-spacing) * ($y - 1)); top: round(($tile-size + $grid-spacing) * ($y - 1));
} }
} }
} }
} }
} }
   
// End of game-field mixin // End of game-field mixin
@include game-field; @include game-field;
   
.tile { .tile {
border-radius: $tile-border-radius; border-radius: $tile-border-radius;
   
background: $tile-color; background: $tile-color;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
z-index: 10; z-index: 10;
   
font-size: 55px; font-size: 55px;
   
@include transition($transition-speed ease-in-out); @include transition($transition-speed ease-in-out);
@include transition-property(top, left); @include transition-property(top, left);
   
$base: 2; $base: 2;
$exponent: 1; $exponent: 1;
$limit: 11; $limit: 11;
   
// Colors for all 11 states, false = no special color // Colors for all 11 states, false = no special color
$special-colors: false false, // 2 $special-colors: false false, // 2
false false, // 4 false false, // 4
#f78e48 true, // 8 #f78e48 true, // 8
#fc5e2e true, // 16 #fc5e2e true, // 16
#ff3333 true, // 32 #ff3333 true, // 32
#ff0000 true, // 64 #ff0000 true, // 64
false true, // 128 false true, // 128
false true, // 256 false true, // 256
false true, // 512 false true, // 512
false true, // 1024 false true, // 1024
false true; // 2048 false true; // 2048
   
// Build tile colors // Build tile colors
@while $exponent <= $limit { @while $exponent <= $limit {
$power: pow($base, $exponent); $power: pow($base, $exponent);
   
&.tile-#{$power} { &.tile-#{$power} {
// Calculate base background color // Calculate base background color
$gold-percent: ($exponent - 1) / ($limit - 1) * 100; $gold-percent: ($exponent - 1) / ($limit - 1) * 100;
$mixed-background: mix($tile-gold-color, $tile-color, $gold-percent); $mixed-background: mix($tile-gold-color, $tile-color, $gold-percent);
   
$nth-color: nth($special-colors, $exponent); $nth-color: nth($special-colors, $exponent);
   
$special-background: nth($nth-color, 1); $special-background: nth($nth-color, 1);
$bright-color: nth($nth-color, 2); $bright-color: nth($nth-color, 2);
   
@if $special-background { @if $special-background {
$mixed-background: mix($special-background, $mixed-background, 55%); $mixed-background: mix($special-background, $mixed-background, 55%);
} }
   
@if $bright-color { @if $bright-color {
color: $bright-text-color; color: $bright-text-color;
} }
   
// Set background // Set background
background: $mixed-background; background: $mixed-background;
   
// Add glow // Add glow
$glow-opacity: max($exponent - 4, 0) / ($limit - 4); $glow-opacity: max($exponent - 4, 0) / ($limit - 4);
   
@if not $special-background { @if not $special-background {
box-shadow: 0 0 30px 10px rgba($tile-gold-glow-color, $glow-opacity / 1.8), 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); inset 0 0 0 1px rgba(white, $glow-opacity / 3);
} }
   
// Adjust font size for bigger numbers // Adjust font size for bigger numbers
@if $power >= 100 and $power < 1000 { @if $power >= 100 and $power < 1000 {
font-size: 45px; font-size: 45px;
   
// Media queries placed here to avoid carrying over the rest of the logic // Media queries placed here to avoid carrying over the rest of the logic
@include smaller(480px) { @include smaller(480px) {
font-size: 25px; font-size: 25px;
} }
} @else if $power >= 1000 { } @else if $power >= 1000 {
font-size: 35px; font-size: 35px;
   
@include smaller(480px) { @include smaller(480px) {
font-size: 15px; font-size: 15px;
} }
} }
} }
   
$exponent: $exponent + 1; $exponent: $exponent + 1;
} }
} }
   
@include keyframes(appear) { @include keyframes(appear) {
0% { 0% {
opacity: 0; opacity: 0;
-webkit-transform: scale(0); -webkit-transform: scale(0);
-moz-transform: scale(0); -moz-transform: scale(0);
} }
   
100% { 100% {
opacity: 1; opacity: 1;
-webkit-transform: scale(1); -webkit-transform: scale(1);
-moz-transform: scale(1); -moz-transform: scale(1);
} }
} }
   
.tile-new { .tile-new {
@include animation(appear 200ms ease $transition-speed); @include animation(appear 200ms ease $transition-speed);
@include animation-fill-mode(both); @include animation-fill-mode(both);
} }
   
@include keyframes(pop) { @include keyframes(pop) {
0% { 0% {
-webkit-transform: scale(0); -webkit-transform: scale(0);
-moz-transform: scale(0); -moz-transform: scale(0);
} }
   
50% { 50% {
-webkit-transform: scale(1.2); -webkit-transform: scale(1.2);
-moz-transform: scale(1.2); -moz-transform: scale(1.2);
} }
   
100% { 100% {
-webkit-transform: scale(1); -webkit-transform: scale(1);
-moz-transform: scale(1); -moz-transform: scale(1);
} }
} }
   
.tile-merged { .tile-merged {
z-index: 20; z-index: 20;
@include animation(pop 200ms ease $transition-speed); @include animation(pop 200ms ease $transition-speed);
@include animation-fill-mode(both); @include animation-fill-mode(both);
} }
   
.game-intro { .game-intro {
margin-bottom: 0; margin-bottom: 0;
} }
   
.game-explanation { .game-explanation {
margin-top: 50px; margin-top: 50px;
} }
   
@include smaller(480px) { @include smaller(480px) {
// Redefine variables for smaller screens // Redefine variables for smaller screens
$field-width: 280px; $field-width: 280px;
$grid-spacing: 10px; $grid-spacing: 10px;
$grid-row-cells: 4; $grid-row-cells: 4;
$tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells; $tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells;
$tile-border-radius: 3px; $tile-border-radius: 3px;
   
html, body { html, body {
font-size: 15px; font-size: 15px;
} }
   
body { body {
margin: 20px 0; margin: 20px 0;
padding: 0 20px; padding: 0 20px;
} }
   
h1.title { h1.title {
font-size: 27px; font-size: 27px;
margin-top: 15px; margin-top: 15px;
} }
   
.container { .container {
width: $field-width; width: $field-width;
margin: 0 auto; margin: 0 auto;
} }
   
// .scores-container { // .scores-container {
// float: left; // float: left;
// clear: left; // clear: left;
// } // }
   
.score-container, .best-container { .score-container, .best-container {
margin-top: 0; margin-top: 0;
padding: 15px 10px; padding: 15px 10px;
min-width: 40px; min-width: 40px;
} }
   
.heading { .heading {
margin-bottom: 10px; margin-bottom: 10px;
} }
   
// Render the game field at the right width // Render the game field at the right width
@include game-field; @include game-field;
   
.game-container { .game-container {
margin-top: 20px; margin-top: 20px;
} }
   
// Rest of the font-size adjustments in the tile class // Rest of the font-size adjustments in the tile class
.tile { .tile {
font-size: 35px; font-size: 35px;
} }
   
.game-message { .game-message {
p { p {
font-size: 30px !important; font-size: 30px !important;
height: 30px !important; height: 30px !important;
line-height: 30px !important; line-height: 30px !important;
margin-top: 90px !important; margin-top: 90px !important;
} }
   
.lower { .lower {
margin-top: 30px !important; margin-top: 30px !important;
} }
} }
} }
   
comments