Added persistence of game state to localstorage after each move
Added persistence of game state to localstorage after each move
Added rebuilding of game state from localstoage on page load
Added New Game button to allow game restarts now that refreshes resume the game

file:a/README.md -> file:b/README.md
--- a/README.md
+++ b/README.md
@@ -3,12 +3,21 @@
 
 Made just for fun. [Play it here!](http://gabrielecirulli.github.io/2048/)
 
+### Contributions
+
+ - [TimPetricola](https://github.com/TimPetricola) added best score storage
+ - [chrisprice](https://github.com/chrisprice) added custom code for swipe handling on mobile
+
+Many thanks to [rayhaanj](https://github.com/rayhaanj), [Mechazawa](https://github.com/Mechazawa), [grant](https://github.com/grant), [remram44](https://github.com/remram44) and [ghoullier](https://github.com/ghoullier) for the many other good contributions.
+
+### Screenshot
+
 [![Screenshot](http://pictures.gabrielecirulli.com/2048-20140309-234100.png)](http://pictures.gabrielecirulli.com/2048-20140309-234100.png)
 
-That screenshot is fake by, the way. I never reached 2048 :smile:
+That screenshot is fake, by the way. I never reached 2048 :smile:
 
 ## Contributing
-Changes and improvements are more than welcome! Feel free to fork and open a pull request. Please make your changes in a specifically made branch and request to pull on `master`! If you can, please make sure the game fully works before sending the PR, as that will help speed up the process.
+Changes and improvements are more than welcome! Feel free to fork and open a pull request. Please make your changes in a specific branch and request to pull into `master`! If you can, please make sure the game fully works before sending the PR, as that will help speed up the process.
 
 You can find the same information in the [contributing guide.](https://github.com/gabrielecirulli/2048/blob/master/CONTRIBUTING.md)
 
@@ -16,5 +25,5 @@
 2048 is licensed under the [MIT license.](https://github.com/gabrielecirulli/2048/blob/master/LICENSE.txt)
 
 ## Donations
-I made this in my spare time, and it's hosted on GitHub (which means I don't have any hosting costs), but if you enjoyed the game and feel like you'd like to buy me a coffee, you can me donate at the `1Ec6onfsQmoP9kkL3zkpB6c5sA4PVcXU2i` BTC address. Thank you very much!
+I made this in my spare time, and it's hosted on GitHub (which means I don't have any hosting costs), but if you enjoyed the game and feel like buying me coffee, you can donate at my BTC address: `1Ec6onfsQmoP9kkL3zkpB6c5sA4PVcXU2i`. Thank you very much!
 

file:a/index.html -> file:b/index.html
--- a/index.html
+++ b/index.html
@@ -5,31 +5,30 @@
   <title>2048</title>
 
   <link href="style/main.css" rel="stylesheet" type="text/css">
-
-  <script src="js/hammer.min.js"></script>
-  <script src="js/keyboard_input_manager.js"></script>
-  <script src="js/html_actuator.js"></script>
-  <script src="js/grid.js"></script>
-  <script src="js/tile.js"></script>
-  <script src="js/game_manager.js"></script>
-  <script src="js/application.js"></script>
+  <link rel="shortcut icon" href="favicon.ico">
+  <link rel="apple-touch-icon" href="meta/apple-touch-icon.png">
+  <meta name="apple-mobile-web-app-capable" content="yes">
 
   <meta name="HandheldFriendly" content="True">
   <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, minimal-ui">
 </head>
 <body>
   <div class="container">
     <div class="heading">
       <h1 class="title">2048</h1>
-      <div class="score-container">0</div>
+      <div class="scores-container">
+        <div class="score-container">0</div>
+        <div class="best-container">0</div>
+      </div>
     </div>
     <p class="game-intro">Join the numbers and get to the <strong>2048 tile!</strong></p>
-
+    <a class="restart-button">New Game</a>
     <div class="game-container">
       <div class="game-message">
         <p></p>
         <div class="lower">
+	        <a class="keep-playing-button">Keep going</a>
           <a class="retry-button">Try again</a>
         </div>
       </div>
@@ -71,9 +70,22 @@
     </p>
     <hr>
     <p>
+    <strong class="important">Note:</strong> This site is the official version of 2048. You can play it on your phone via <a href="http://git.io/2048">http://git.io/2048.</a> All other apps or sites are derivatives or fakes, and should be used with caution.
+    </p>
+    <hr>
+    <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>
     </p>
   </div>
+
+  <script src="js/animframe_polyfill.js"></script>
+  <script src="js/keyboard_input_manager.js"></script>
+  <script src="js/html_actuator.js"></script>
+  <script src="js/grid.js"></script>
+  <script src="js/tile.js"></script>
+  <script src="js/local_score_manager.js"></script>
+  <script src="js/game_manager.js"></script>
+  <script src="js/application.js"></script>
 </body>
 </html>
 

--- /dev/null
+++ b/js/animframe_polyfill.js
@@ -1,1 +1,27 @@
+(function() {
+  var lastTime = 0;
+  var vendors = ['webkit', 'moz'];
+  for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+    window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
+    window.cancelAnimationFrame =
+    window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
+  }
 
+  if (!window.requestAnimationFrame) {
+    window.requestAnimationFrame = function(callback, element) {
+      var currTime = new Date().getTime();
+      var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+      var id = window.setTimeout(function() { callback(currTime + timeToCall); },
+      timeToCall);
+      lastTime = currTime + timeToCall;
+      return id;
+    };
+  }
+
+  if (!window.cancelAnimationFrame) {
+    window.cancelAnimationFrame = function(id) {
+      clearTimeout(id);
+    };
+  }
+}());
+

--- a/js/application.js
+++ b/js/application.js
@@ -1,7 +1,5 @@
-document.addEventListener("DOMContentLoaded", function () {
-  // Wait till the browser is ready to render the game (avoids glitches)
-  window.requestAnimationFrame(function () {
-    var manager = new GameManager(4, KeyboardInputManager, HTMLActuator);
-  });
+// Wait till the browser is ready to render the game (avoids glitches)
+window.requestAnimationFrame(function () {
+  new GameManager(4, KeyboardInputManager, HTMLActuator, LocalScoreManager);
 });
 

--- a/js/game_manager.js
+++ b/js/game_manager.js
@@ -1,32 +1,59 @@
-function GameManager(size, InputManager, Actuator) {
+function GameManager(size, InputManager, Actuator, StorageManager) {
   this.size         = size; // Size of the grid
   this.inputManager = new InputManager;
+  this.storageManager = new StorageManager;
   this.actuator     = new Actuator;
 
   this.startTiles   = 2;
 
   this.inputManager.on("move", this.move.bind(this));
   this.inputManager.on("restart", this.restart.bind(this));
+  this.inputManager.on("keepPlaying", this.keepPlaying.bind(this));
 
   this.setup();
 }
 
 // Restart the game
 GameManager.prototype.restart = function () {
-  this.actuator.restart();
+  this.storageManager.clearGameState();
+  this.actuator.continue();
   this.setup();
+};
+
+// Keep playing after winning
+GameManager.prototype.keepPlaying = function () {
+  this.keepPlaying = true;
+  this.actuator.continue();
+};
+
+GameManager.prototype.isGameTerminated = function () {
+  if (this.over || (this.won && !this.keepPlaying)) {
+    return true;
+  } else {
+    return false;
+  }
 };
 
 // Set up the game
 GameManager.prototype.setup = function () {
-  this.grid         = new Grid(this.size);
-
-  this.score        = 0;
-  this.over         = false;
-  this.won          = false;
-
-  // Add the initial tiles
-  this.addStartTiles();
+  var previousGameState = this.storageManager.getGameState();
+
+  if (previousGameState) {
+      this.grid        = new Grid(previousGameState.grid.size, previousGameState.grid.cells);
+      this.score       = previousGameState.score;
+      this.over        = previousGameState.over;
+      this.won         = previousGameState.won;
+      this.keepPlaying = previousGameState.keepPlaying;
+  } else {
+      this.grid        = new Grid(this.size);
+      this.score       = 0;
+      this.over        = false;
+      this.won         = false;
+      this.keepPlaying = false;
+
+      // Add the initial tiles
+      this.addStartTiles();
+  }
 
   // Update the actuator
   this.actuate();
@@ -51,12 +78,32 @@
 
 // Sends the updated grid to the actuator
 GameManager.prototype.actuate = function () {
+  if (this.storageManager.getBestScore() < this.score) {
+    this.storageManager.setBestScore(this.score);
+  }
+
+  this.storageManager.setGameState(this.serializeGameState());
+
   this.actuator.actuate(this.grid, {
-    score: this.score,
-    over:  this.over,
-    won:   this.won
+    score:      this.score,
+    over:       this.over,
+    won:        this.won,
+    bestScore:  this.storageManager.getBestScore(),
+    terminated: this.isGameTerminated()
   });
-};
+
+};
+
+GameManager.prototype.serializeGameState = function () {
+    return {
+        size:        this.size,
+        grid:        this.grid.gridState(),
+        score:       this.score,
+        over:        this.over,
+        won:         this.won,
+        keepPlaying: this.keepPlaying
+    };
+}
 
 // Save all tile positions and remove merger info
 GameManager.prototype.prepareTiles = function () {
@@ -80,7 +127,7 @@
   // 0: up, 1: right, 2:down, 3: left
   var self = this;
 
-  if (this.over || this.won) return; // Don't do anything if the game's over
+  if (this.isGameTerminated()) return; // Don't do anything if the game's over
 
   var cell, tile;
 
@@ -204,8 +251,6 @@
           var cell   = { x: x + vector.x, y: y + vector.y };
 
           var other  = self.grid.cellContent(cell);
-          if (other) {
-          }
 
           if (other && other.value === tile.value) {
             return true; // These two tiles can be merged

file:a/js/grid.js -> file:b/js/grid.js
--- a/js/grid.js
+++ b/js/grid.js
@@ -1,20 +1,32 @@
-function Grid(size) {
+function Grid(size, previousCellState) {
   this.size = size;
-
-  this.cells = [];
-
-  this.build();
+  this.cells = previousCellState ? this.buildFromPreviousState(previousCellState) : this.buildNew();
 }
 
 // Build a grid of the specified size
-Grid.prototype.build = function () {
+Grid.prototype.buildNew = function () {
+  var cells = [];
   for (var x = 0; x < this.size; x++) {
-    var row = this.cells[x] = [];
+    var row = cells[x] = [];
 
     for (var y = 0; y < this.size; y++) {
       row.push(null);
     }
   }
+  return cells;
+};
+
+Grid.prototype.buildFromPreviousState = function (state) {
+    var cells = [];
+    for (var x = 0; x < this.size; x++) {
+        var row = cells[x] = [];
+
+        for (var y = 0; y < this.size; y++) {
+            var tileState = state[x][y];
+            row.push(tileState ? new Tile(tileState.position, tileState.value) : null);
+        }
+    }
+    return cells;
 };
 
 // Find the first available random position
@@ -83,3 +95,19 @@
          position.y >= 0 && position.y < this.size;
 };
 
+Grid.prototype.gridState = function () {
+  var cellState = [];
+  for (var x = 0; x < this.size; x++) {
+    var row = cellState[x] = [];
+
+    for (var y = 0; y < this.size; y++) {
+        row.push(this.cells[x][y] ? this.cells[x][y].tileState() : null);
+    }
+  }
+
+  return {
+      size: this.size,
+      cells: cellState
+  }
+};
+

file:a/js/hammer.min.js (deleted)
--- a/js/hammer.min.js
+++ /dev/null
@@ -1,9 +1,1 @@
-/*! Hammer.JS - v1.0.6 - 2014-01-02
- * http://eightmedia.github.com/hammer.js
- *
- * Copyright (c) 2014 Jorik Tangelder <j.tangelder@gmail.com>;
- * Licensed under the MIT license */
 
-
-!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.index<b.index?-1:a.index>b.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.distance<b.options.drag_min_distance&&d.detection.current.name!=this.name)return;if(d.detection.current.name!=this.name&&(d.detection.current.name=this.name,b.options.correct_for_drag_min_distance&&a.distance>0)){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.length<b.options.swipe_min_touches&&a.touches.length>b.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.timeStamp<b.options.doubletap_interval&&a.distance<b.options.doubletap_distance&&(b.trigger("doubletap",a),e=!0),(!e||b.options.tap_always)&&(d.detection.current.name="tap",b.trigger(d.detection.current.name,a))}}},d.gestures.Touch={name:"touch",index:-1/0,defaults:{prevent_default:!1,prevent_mouseevents:!1},handler:function(a,b){return b.options.prevent_mouseevents&&a.pointerType==d.POINTER_MOUSE?(a.stopDetect(),void 0):(b.options.prevent_default&&a.preventDefault(),a.eventType==d.EVENT_START&&b.trigger(this.name,a),void 0)}},d.gestures.Transform={name:"transform",index:45,defaults:{transform_min_scale:.01,transform_min_rotation:1,transform_always_block:!1},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(!(a.touches.length<2))switch(b.options.transform_always_block&&a.preventDefault(),a.eventType){case d.EVENT_START:this.triggered=!1;break;case d.EVENT_MOVE:var c=Math.abs(1-a.scale),e=Math.abs(a.rotation);if(c<b.options.transform_min_scale&&e<b.options.transform_min_rotation)return;d.detection.current.name=this.name,this.triggered||(b.trigger(this.name+"start",a),this.triggered=!0),b.trigger(this.name,a),e>b.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);
-//# sourceMappingURL=hammer.min.map

--- a/js/html_actuator.js
+++ b/js/html_actuator.js
@@ -1,7 +1,8 @@
 function HTMLActuator() {
-  this.tileContainer    = document.getElementsByClassName("tile-container")[0];
-  this.scoreContainer   = document.getElementsByClassName("score-container")[0];
-  this.messageContainer = document.getElementsByClassName("game-message")[0];
+  this.tileContainer    = document.querySelector(".tile-container");
+  this.scoreContainer   = document.querySelector(".score-container");
+  this.bestContainer    = document.querySelector(".best-container");
+  this.messageContainer = document.querySelector(".game-message");
 
   this.score = 0;
 }
@@ -21,13 +22,21 @@
     });
 
     self.updateScore(metadata.score);
+    self.updateBestScore(metadata.bestScore);
 
-    if (metadata.over) self.message(false); // You lose
-    if (metadata.won) self.message(true); // You win!
+    if (metadata.terminated) {
+      if (metadata.over) {
+        self.message(false); // You lose
+      } else if (metadata.won) {
+        self.message(true); // You win!
+      }
+    }
+
   });
 };
 
-HTMLActuator.prototype.restart = function () {
+// Continues the game (both restart and keep playing)
+HTMLActuator.prototype.continue = function () {
   this.clearMessage();
 };
 
@@ -40,25 +49,30 @@
 HTMLActuator.prototype.addTile = function (tile) {
   var self = this;
 
-  var element   = document.createElement("div");
+  var wrapper   = document.createElement("div");
+  var inner     = document.createElement("div");
   var position  = tile.previousPosition || { x: tile.x, y: tile.y };
-  positionClass = this.positionClass(position);
+  var positionClass = this.positionClass(position);
 
   // We can't use classlist because it somehow glitches when replacing classes
   var classes = ["tile", "tile-" + tile.value, positionClass];
-  this.applyClasses(element, classes);
 
-  element.textContent = tile.value;
+  if (tile.value > 2048) classes.push("tile-super");
+
+  this.applyClasses(wrapper, classes);
+
+  inner.classList.add("tile-inner");
+  inner.textContent = tile.value;
 
   if (tile.previousPosition) {
     // Make sure that the tile gets rendered in the previous position first
     window.requestAnimationFrame(function () {
       classes[2] = self.positionClass({ x: tile.x, y: tile.y });
-      self.applyClasses(element, classes); // Update the position
+      self.applyClasses(wrapper, classes); // Update the position
     });
   } else if (tile.mergedFrom) {
     classes.push("tile-merged");
-    this.applyClasses(element, classes);
+    this.applyClasses(wrapper, classes);
 
     // Render the tiles that merged
     tile.mergedFrom.forEach(function (merged) {
@@ -66,11 +80,14 @@
     });
   } else {
     classes.push("tile-new");
-    this.applyClasses(element, classes);
+    this.applyClasses(wrapper, classes);
   }
 
+  // Add the inner part of the tile to the wrapper
+  wrapper.appendChild(inner);
+
   // Put the tile on the board
-  this.tileContainer.appendChild(element);
+  this.tileContainer.appendChild(wrapper);
 };
 
 HTMLActuator.prototype.applyClasses = function (element, classes) {
@@ -103,17 +120,21 @@
   }
 };
 
+HTMLActuator.prototype.updateBestScore = function (bestScore) {
+  this.bestContainer.textContent = bestScore;
+};
+
 HTMLActuator.prototype.message = function (won) {
   var type    = won ? "game-won" : "game-over";
-  var message = won ? "You win!" : "Game over!"
-
-  // if (ga) ga("send", "event", "game", "end", type, this.score);
+  var message = won ? "You win!" : "Game over!";
 
   this.messageContainer.classList.add(type);
   this.messageContainer.getElementsByTagName("p")[0].textContent = message;
 };
 
 HTMLActuator.prototype.clearMessage = function () {
-  this.messageContainer.classList.remove("game-won", "game-over");
+  // IE only takes one value to remove at a time.
+  this.messageContainer.classList.remove("game-won");
+  this.messageContainer.classList.remove("game-over");
 };
 

--- a/js/keyboard_input_manager.js
+++ b/js/keyboard_input_manager.js
@@ -31,7 +31,11 @@
     75: 0, // vim keybindings
     76: 1,
     74: 2,
-    72: 3
+    72: 3,
+    87: 0, // W
+    68: 1, // D
+    83: 2, // S
+    65: 3  // A
   };
 
   document.addEventListener("keydown", function (event) {
@@ -49,24 +53,47 @@
     }
   });
 
-  var retry = document.getElementsByClassName("retry-button")[0];
+  var retry = document.querySelector(".retry-button");
   retry.addEventListener("click", this.restart.bind(this));
+  retry.addEventListener("touchend", this.restart.bind(this));
+
+  var restart = document.querySelector(".restart-button");
+  restart.addEventListener("click", this.restart.bind(this));
+  restart.addEventListener("touchend", this.restart.bind(this));
+
+  var keepPlaying = document.querySelector(".keep-playing-button");
+  keepPlaying.addEventListener("click", this.keepPlaying.bind(this));
+  keepPlaying.addEventListener("touchend", this.keepPlaying.bind(this));
 
   // Listen to swipe events
-  var gestures = [Hammer.DIRECTION_UP, Hammer.DIRECTION_RIGHT,
-                  Hammer.DIRECTION_DOWN, Hammer.DIRECTION_LEFT];
+  var touchStartClientX, touchStartClientY;
+  var gameContainer = document.getElementsByClassName("game-container")[0];
 
-  var gameContainer = document.getElementsByClassName("game-container")[0];
-  var handler       = Hammer(gameContainer, {
-    drag_block_horizontal: true,
-    drag_block_vertical: true
+  gameContainer.addEventListener("touchstart", function (event) {
+    if (event.touches.length > 1) return;
+
+    touchStartClientX = event.touches[0].clientX;
+    touchStartClientY = event.touches[0].clientY;
+    event.preventDefault();
   });
-  
-  handler.on("swipe", function (event) {
-    event.gesture.preventDefault();
-    mapped = gestures.indexOf(event.gesture.direction);
 
-    if (mapped !== -1) self.emit("move", mapped);
+  gameContainer.addEventListener("touchmove", function (event) {
+    event.preventDefault();
+  });
+
+  gameContainer.addEventListener("touchend", function (event) {
+    if (event.touches.length > 0) return;
+
+    var dx = event.changedTouches[0].clientX - touchStartClientX;
+    var absDx = Math.abs(dx);
+
+    var dy = event.changedTouches[0].clientY - touchStartClientY;
+    var absDy = Math.abs(dy);
+
+    if (Math.max(absDx, absDy) > 10) {
+      // (right : left) : (down : up)
+      self.emit("move", absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0));
+    }
   });
 };
 
@@ -75,3 +102,8 @@
   this.emit("restart");
 };
 
+KeyboardInputManager.prototype.keepPlaying = function (event) {
+  event.preventDefault();
+  this.emit("keepPlaying");
+};
+

--- /dev/null
+++ b/js/local_score_manager.js
@@ -1,1 +1,62 @@
+window.fakeStorage = {
+  _data: {},
 
+  setItem: function (id, val) {
+    return this._data[id] = String(val);
+  },
+
+  getItem: function (id) {
+    return this._data.hasOwnProperty(id) ? this._data[id] : undefined;
+  },
+
+  removeItem: function (id) {
+    return delete this._data[id];
+  },
+
+  clear: function () {
+    return this._data = {};
+  }
+};
+
+function LocalScoreManager() {
+  this.bestScoreKey     = "bestScore";
+  this.gameStateKey     = "gameState";
+
+  var supported = this.localStorageSupported();
+  this.storage = supported ? window.localStorage : window.fakeStorage;
+}
+
+LocalScoreManager.prototype.localStorageSupported = function () {
+  var testKey = "test";
+  var storage = window.localStorage;
+
+  try {
+    storage.setItem(testKey, "1");
+    storage.removeItem(testKey);
+    return true;
+  } catch (error) {
+    return false;
+  }
+};
+
+LocalScoreManager.prototype.getBestScore = function () {
+  return this.storage.getItem(this.bestScoreKey) || 0;
+};
+
+LocalScoreManager.prototype.setBestScore = function (score) {
+  this.storage.setItem(this.bestScoreKey, score);
+};
+
+LocalScoreManager.prototype.getGameState = function () {
+    var stateJSON = this.storage.getItem(this.gameStateKey);
+    return stateJSON ? JSON.parse(stateJSON) : null;
+};
+
+LocalScoreManager.prototype.setGameState = function (gameState) {
+    this.storage.setItem(this.gameStateKey, JSON.stringify(gameState));
+};
+
+LocalScoreManager.prototype.clearGameState = function () {
+    this.storage.removeItem(this.gameStateKey);
+};
+

file:a/js/tile.js -> file:b/js/tile.js
--- a/js/tile.js
+++ b/js/tile.js
@@ -16,3 +16,12 @@
   this.y = position.y;
 };
 
+Tile.prototype.tileState = function () {
+    return {
+        position: {
+            x: this.x,
+            y: this.y
+        },
+        value: this.value
+    };
+}

 Binary files /dev/null and b/meta/apple-touch-icon.png differ
--- a/style/helpers.scss
+++ b/style/helpers.scss
@@ -24,11 +24,31 @@
 @mixin transition($args...) {
   -webkit-transition: $args;
   -moz-transition: $args;
+  transition: $args;
 }
 
 @mixin transition-property($args...) {
   -webkit-transition-property: $args;
   -moz-transition-property: $args;
+  transition-property: $args;
+}
+
+@mixin animation($args...) {
+  -webkit-animation: $args;
+  -moz-animation: $args;
+  animation: $args;
+}
+
+@mixin animation-fill-mode($args...) {
+  -webkit-animation-fill-mode: $args;
+  -moz-animation-fill-mode: $args;
+  animation-fill-mode: $args;
+}
+
+@mixin transform($args...) {
+  -webkit-transform: $args;
+  -moz-transform: $args;
+  transform: $args;
 }
 
 // Keyframe animations
@@ -44,16 +64,6 @@
   }
 }
 
-@mixin animation($str) {
-  -webkit-animation: #{$str};
-  -moz-animation: #{$str};
-}
-
-@mixin animation-fill-mode($str) {
-  -webkit-animation-fill-mode: #{$str};
-  -moz-animation-fill-mode: #{$str};
-}
-
 // Media queries
 @mixin smaller($width) {
   @media screen and (max-width: $width) {

--- a/style/main.css
+++ b/style/main.css
@@ -49,9 +49,13 @@
     top: -50px;
     opacity: 0; } }
 
-.score-container {
+.scores-container {
+  float: right;
+  text-align: right; }
+
+.score-container, .best-container {
   position: relative;
-  float: right;
+  display: inline-block;
   background: #bbada0;
   padding: 15px 25px;
   font-size: 25px;
@@ -60,19 +64,19 @@
   font-weight: bold;
   border-radius: 3px;
   color: white;
-  margin-top: 8px; }
-  .score-container:after {
+  margin-top: 8px;
+  text-align: center; }
+  .score-container:after, .best-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 {
+  .score-container .score-addition, .best-container .score-addition {
     position: absolute;
     right: 30px;
     color: red;
@@ -83,8 +87,16 @@
     z-index: 100;
     -webkit-animation: move-up 600ms ease-in;
     -moz-animation: move-up 600ms ease-in;
+    animation: move-up 600ms ease-in;
     -webkit-animation-fill-mode: both;
-    -moz-animation-fill-mode: both; }
+    -moz-animation-fill-mode: both;
+    animation-fill-mode: both; }
+
+.score-container:after {
+  content: "Score"; }
+
+.best-container:after {
+  content: "Best"; }
 
 p {
   margin-top: 0;
@@ -131,8 +143,21 @@
   100% {
     opacity: 1; } }
 
+.restart-button {
+  display: inline-block;
+  background: #8f7a66;
+  border-radius: 3px;
+  padding: 0 20px;
+  text-decoration: none;
+  color: #f9f6f2;
+  height: 40px;
+  line-height: 42px;
+  display: block;
+  width: 100px;
+  margin: 10px auto 10px auto;
+  text-align: center; }
+
 .game-container {
-  margin-top: 40px;
   position: relative;
   padding: 15px;
   cursor: default;
@@ -158,8 +183,10 @@
     text-align: center;
     -webkit-animation: fade-in 800ms ease 1200ms;
     -moz-animation: fade-in 800ms ease 1200ms;
+    animation: fade-in 800ms ease 1200ms;
     -webkit-animation-fill-mode: both;
-    -moz-animation-fill-mode: both; }
+    -moz-animation-fill-mode: both;
+    animation-fill-mode: both; }
     .game-container .game-message p {
       font-size: 60px;
       font-weight: bold;
@@ -179,9 +206,13 @@
       height: 40px;
       line-height: 42px;
       margin-left: 9px; }
+      .game-container .game-message a.keep-playing-button {
+        display: none; }
     .game-container .game-message.game-won {
       background: rgba(237, 194, 46, 0.5);
       color: #f9f6f2; }
+      .game-container .game-message.game-won a.keep-playing-button {
+        display: inline-block; }
     .game-container .game-message.game-won, .game-container .game-message.game-over {
       display: block; }
 
@@ -212,229 +243,259 @@
   position: absolute;
   z-index: 2; }
 
+.tile, .tile .tile-inner {
+  width: 107px;
+  height: 107px;
+  line-height: 116.25px; }
+.tile.tile-position-1-1 {
+  -webkit-transform: translate(0px, 0px);
+  -moz-transform: translate(0px, 0px);
+  transform: translate(0px, 0px); }
+.tile.tile-position-1-2 {
+  -webkit-transform: translate(0px, 121px);
+  -moz-transform: translate(0px, 121px);
+  transform: translate(0px, 121px); }
+.tile.tile-position-1-3 {
+  -webkit-transform: translate(0px, 242px);
+  -moz-transform: translate(0px, 242px);
+  transform: translate(0px, 242px); }
+.tile.tile-position-1-4 {
+  -webkit-transform: translate(0px, 363px);
+  -moz-transform: translate(0px, 363px);
+  transform: translate(0px, 363px); }
+.tile.tile-position-2-1 {
+  -webkit-transform: translate(121px, 0px);
+  -moz-transform: translate(121px, 0px);
+  transform: translate(121px, 0px); }
+.tile.tile-position-2-2 {
+  -webkit-transform: translate(121px, 121px);
+  -moz-transform: translate(121px, 121px);
+  transform: translate(121px, 121px); }
+.tile.tile-position-2-3 {
+  -webkit-transform: translate(121px, 242px);
+  -moz-transform: translate(121px, 242px);
+  transform: translate(121px, 242px); }
+.tile.tile-position-2-4 {
+  -webkit-transform: translate(121px, 363px);
+  -moz-transform: translate(121px, 363px);
+  transform: translate(121px, 363px); }
+.tile.tile-position-3-1 {
+  -webkit-transform: translate(242px, 0px);
+  -moz-transform: translate(242px, 0px);
+  transform: translate(242px, 0px); }
+.tile.tile-position-3-2 {
+  -webkit-transform: translate(242px, 121px);
+  -moz-transform: translate(242px, 121px);
+  transform: translate(242px, 121px); }
+.tile.tile-position-3-3 {
+  -webkit-transform: translate(242px, 242px);
+  -moz-transform: translate(242px, 242px);
+  transform: translate(242px, 242px); }
+.tile.tile-position-3-4 {
+  -webkit-transform: translate(242px, 363px);
+  -moz-transform: translate(242px, 363px);
+  transform: translate(242px, 363px); }
+.tile.tile-position-4-1 {
+  -webkit-transform: translate(363px, 0px);
+  -moz-transform: translate(363px, 0px);
+  transform: translate(363px, 0px); }
+.tile.tile-position-4-2 {
+  -webkit-transform: translate(363px, 121px);
+  -moz-transform: translate(363px, 121px);
+  transform: translate(363px, 121px); }
+.tile.tile-position-4-3 {
+  -webkit-transform: translate(363px, 242px);
+  -moz-transform: translate(363px, 242px);
+  transform: translate(363px, 242px); }
+.tile.tile-position-4-4 {
+  -webkit-transform: translate(363px, 363px);
+  -moz-transform: translate(363px, 363px);
+  transform: translate(363px, 363px); }
+
 .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;
+  position: absolute;
   -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 {
+  transition: 100ms ease-in-out;
+  -webkit-transition-property: -webkit-transform;
+  -moz-transition-property: -moz-transform;
+  transition-property: transform; }
+  .tile .tile-inner {
+    border-radius: 3px;
+    background: #eee4da;
+    text-align: center;
+    font-weight: bold;
+    z-index: 10;
+    font-size: 55px; }
+  .tile.tile-2 .tile-inner {
     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 {
+  .tile.tile-4 .tile-inner {
     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 {
+  .tile.tile-8 .tile-inner {
     color: #f9f6f2;
     background: #f2b179; }
-  .tile.tile-16 {
+  .tile.tile-16 .tile-inner {
     color: #f9f6f2;
     background: #f59563; }
-  .tile.tile-32 {
+  .tile.tile-32 .tile-inner {
     color: #f9f6f2;
     background: #f67c5f; }
-  .tile.tile-64 {
+  .tile.tile-64 .tile-inner {
     color: #f9f6f2;
     background: #f65e3b; }
-  .tile.tile-128 {
+  .tile.tile-128 .tile-inner {
     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 {
+    @media screen and (max-width: 520px) {
+      .tile.tile-128 .tile-inner {
         font-size: 25px; } }
-  .tile.tile-256 {
+  .tile.tile-256 .tile-inner {
     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 {
+    @media screen and (max-width: 520px) {
+      .tile.tile-256 .tile-inner {
         font-size: 25px; } }
-  .tile.tile-512 {
+  .tile.tile-512 .tile-inner {
     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 {
+    @media screen and (max-width: 520px) {
+      .tile.tile-512 .tile-inner {
         font-size: 25px; } }
-  .tile.tile-1024 {
+  .tile.tile-1024 .tile-inner {
     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 {
+    @media screen and (max-width: 520px) {
+      .tile.tile-1024 .tile-inner {
         font-size: 15px; } }
-  .tile.tile-2048 {
+  .tile.tile-2048 .tile-inner {
     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 {
+    @media screen and (max-width: 520px) {
+      .tile.tile-2048 .tile-inner {
         font-size: 15px; } }
+  .tile.tile-super .tile-inner {
+    color: #f9f6f2;
+    background: #3c3a32;
+    font-size: 30px; }
+    @media screen and (max-width: 520px) {
+      .tile.tile-super .tile-inner {
+        font-size: 10px; } }
 
 @-webkit-keyframes appear {
   0% {
     opacity: 0;
     -webkit-transform: scale(0);
-    -moz-transform: scale(0); }
+    -moz-transform: scale(0);
+    transform: scale(0); }
 
   100% {
     opacity: 1;
     -webkit-transform: scale(1);
-    -moz-transform: scale(1); } }
+    -moz-transform: scale(1);
+    transform: scale(1); } }
 
 @-moz-keyframes appear {
   0% {
     opacity: 0;
     -webkit-transform: scale(0);
-    -moz-transform: scale(0); }
+    -moz-transform: scale(0);
+    transform: scale(0); }
 
   100% {
     opacity: 1;
     -webkit-transform: scale(1);
-    -moz-transform: scale(1); } }
+    -moz-transform: scale(1);
+    transform: scale(1); } }
 
 @keyframes appear {
   0% {
     opacity: 0;
     -webkit-transform: scale(0);
-    -moz-transform: scale(0); }
+    -moz-transform: scale(0);
+    transform: scale(0); }
 
   100% {
     opacity: 1;
     -webkit-transform: scale(1);
-    -moz-transform: scale(1); } }
-
-.tile-new {
+    -moz-transform: scale(1);
+    transform: scale(1); } }
+
+.tile-new .tile-inner {
   -webkit-animation: appear 200ms ease 100ms;
   -moz-animation: appear 200ms ease 100ms;
-  -webkit-animation-fill-mode: both;
-  -moz-animation-fill-mode: both; }
+  animation: appear 200ms ease 100ms;
+  -webkit-animation-fill-mode: backwards;
+  -moz-animation-fill-mode: backwards;
+  animation-fill-mode: backwards; }
 
 @-webkit-keyframes pop {
   0% {
     -webkit-transform: scale(0);
-    -moz-transform: scale(0); }
+    -moz-transform: scale(0);
+    transform: scale(0); }
 
   50% {
     -webkit-transform: scale(1.2);
-    -moz-transform: scale(1.2); }
+    -moz-transform: scale(1.2);
+    transform: scale(1.2); }
 
   100% {
     -webkit-transform: scale(1);
-    -moz-transform: scale(1); } }
+    -moz-transform: scale(1);
+    transform: scale(1); } }
 
 @-moz-keyframes pop {
   0% {
     -webkit-transform: scale(0);
-    -moz-transform: scale(0); }
+    -moz-transform: scale(0);
+    transform: scale(0); }
 
   50% {
     -webkit-transform: scale(1.2);
-    -moz-transform: scale(1.2); }
+    -moz-transform: scale(1.2);
+    transform: scale(1.2); }
 
   100% {
     -webkit-transform: scale(1);
-    -moz-transform: scale(1); } }
+    -moz-transform: scale(1);
+    transform: scale(1); } }
 
 @keyframes pop {
   0% {
     -webkit-transform: scale(0);
-    -moz-transform: scale(0); }
+    -moz-transform: scale(0);
+    transform: scale(0); }
 
   50% {
     -webkit-transform: scale(1.2);
-    -moz-transform: scale(1.2); }
+    -moz-transform: scale(1.2);
+    transform: scale(1.2); }
 
   100% {
     -webkit-transform: scale(1);
-    -moz-transform: scale(1); } }
-
-.tile-merged {
+    -moz-transform: scale(1);
+    transform: scale(1); } }
+
+.tile-merged .tile-inner {
   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; }
+  animation: pop 200ms ease 100ms;
+  -webkit-animation-fill-mode: backwards;
+  -moz-animation-fill-mode: backwards;
+  animation-fill-mode: backwards; }
 
 .game-intro {
   margin-bottom: 0; }
@@ -442,7 +503,7 @@
 .game-explanation {
   margin-top: 50px; }
 
-@media screen and (max-width: 480px) {
+@media screen and (max-width: 520px) {
   html, body {
     font-size: 15px; }
 
@@ -451,20 +512,22 @@
     padding: 0 20px; }
 
   h1.title {
-    font-size: 50px; }
+    font-size: 27px;
+    margin-top: 15px; }
 
   .container {
     width: 280px;
     margin: 0 auto; }
 
-  .score-container {
-    margin-top: 0; }
+  .score-container, .best-container {
+    margin-top: 0;
+    padding: 15px 10px;
+    min-width: 40px; }
 
   .heading {
     margin-bottom: 10px; }
 
   .game-container {
-    margin-top: 40px;
     position: relative;
     padding: 10px;
     cursor: default;
@@ -490,8 +553,10 @@
       text-align: center;
       -webkit-animation: fade-in 800ms ease 1200ms;
       -moz-animation: fade-in 800ms ease 1200ms;
+      animation: fade-in 800ms ease 1200ms;
       -webkit-animation-fill-mode: both;
-      -moz-animation-fill-mode: both; }
+      -moz-animation-fill-mode: both;
+      animation-fill-mode: both; }
       .game-container .game-message p {
         font-size: 60px;
         font-weight: bold;
@@ -511,9 +576,13 @@
         height: 40px;
         line-height: 42px;
         margin-left: 9px; }
+        .game-container .game-message a.keep-playing-button {
+          display: none; }
       .game-container .game-message.game-won {
         background: rgba(237, 194, 46, 0.5);
         color: #f9f6f2; }
+        .game-container .game-message.game-won a.keep-playing-button {
+          display: inline-block; }
       .game-container .game-message.game-won, .game-container .game-message.game-over {
         display: block; }
 
@@ -544,79 +613,79 @@
     position: absolute;
     z-index: 2; }
 
-  .tile {
-    width: 57.5px;
-    height: 57.5px;
+  .tile, .tile .tile-inner {
+    width: 58px;
+    height: 58px;
     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; }
+  .tile.tile-position-1-1 {
+    -webkit-transform: translate(0px, 0px);
+    -moz-transform: translate(0px, 0px);
+    transform: translate(0px, 0px); }
+  .tile.tile-position-1-2 {
+    -webkit-transform: translate(0px, 67px);
+    -moz-transform: translate(0px, 67px);
+    transform: translate(0px, 67px); }
+  .tile.tile-position-1-3 {
+    -webkit-transform: translate(0px, 135px);
+    -moz-transform: translate(0px, 135px);
+    transform: translate(0px, 135px); }
+  .tile.tile-position-1-4 {
+    -webkit-transform: translate(0px, 202px);
+    -moz-transform: translate(0px, 202px);
+    transform: translate(0px, 202px); }
+  .tile.tile-position-2-1 {
+    -webkit-transform: translate(67px, 0px);
+    -moz-transform: translate(67px, 0px);
+    transform: translate(67px, 0px); }
+  .tile.tile-position-2-2 {
+    -webkit-transform: translate(67px, 67px);
+    -moz-transform: translate(67px, 67px);
+    transform: translate(67px, 67px); }
+  .tile.tile-position-2-3 {
+    -webkit-transform: translate(67px, 135px);
+    -moz-transform: translate(67px, 135px);
+    transform: translate(67px, 135px); }
+  .tile.tile-position-2-4 {
+    -webkit-transform: translate(67px, 202px);
+    -moz-transform: translate(67px, 202px);
+    transform: translate(67px, 202px); }
+  .tile.tile-position-3-1 {
+    -webkit-transform: translate(135px, 0px);
+    -moz-transform: translate(135px, 0px);
+    transform: translate(135px, 0px); }
+  .tile.tile-position-3-2 {
+    -webkit-transform: translate(135px, 67px);
+    -moz-transform: translate(135px, 67px);
+    transform: translate(135px, 67px); }
+  .tile.tile-position-3-3 {
+    -webkit-transform: translate(135px, 135px);
+    -moz-transform: translate(135px, 135px);
+    transform: translate(135px, 135px); }
+  .tile.tile-position-3-4 {
+    -webkit-transform: translate(135px, 202px);
+    -moz-transform: translate(135px, 202px);
+    transform: translate(135px, 202px); }
+  .tile.tile-position-4-1 {
+    -webkit-transform: translate(202px, 0px);
+    -moz-transform: translate(202px, 0px);
+    transform: translate(202px, 0px); }
+  .tile.tile-position-4-2 {
+    -webkit-transform: translate(202px, 67px);
+    -moz-transform: translate(202px, 67px);
+    transform: translate(202px, 67px); }
+  .tile.tile-position-4-3 {
+    -webkit-transform: translate(202px, 135px);
+    -moz-transform: translate(202px, 135px);
+    transform: translate(202px, 135px); }
+  .tile.tile-position-4-4 {
+    -webkit-transform: translate(202px, 202px);
+    -moz-transform: translate(202px, 202px);
+    transform: translate(202px, 202px); }
 
   .game-container {
     margin-top: 20px; }
 
-  .tile {
+  .tile .tile-inner {
     font-size: 35px; }
 
   .game-message p {

--- a/style/main.scss
+++ b/style/main.scss
@@ -6,6 +6,8 @@
 $grid-row-cells: 4;
 $tile-size: ($field-width - $grid-spacing * ($grid-row-cells + 1)) / $grid-row-cells;
 $tile-border-radius: 3px;
+
+$mobile-threshold: $field-width + 20px;
 
 $text-color: #776E65;
 $bright-text-color: #f9f6f2;
@@ -58,11 +60,16 @@
   }
 }
 
-.score-container {
+.scores-container {
+  float: right;
+  text-align: right;
+}
+
+.score-container, .best-container {
   $height: 25px;
 
   position: relative;
-  float: right;
+  display: inline-block;
   background: $game-container-background;
   padding: 15px 25px;
   font-size: $height;
@@ -72,13 +79,13 @@
   border-radius: 3px;
   color: white;
   margin-top: 8px;
+  text-align: center;
 
   &:after {
     position: absolute;
     width: 100%;
     top: 10px;
     left: 0;
-    content: "Score";
     text-transform: uppercase;
     font-size: 13px;
     line-height: 13px;
@@ -100,6 +107,14 @@
   }
 }
 
+.score-container:after {
+  content: "Score";
+}
+
+.best-container:after {
+  content: "Best"
+}
+
 p {
   margin-top: 0;
   margin-bottom: 10px;
@@ -141,10 +156,29 @@
   }
 }
 
+// Styles for buttons
+@mixin button {
+  display: inline-block;
+  background: darken($game-container-background, 20%);
+  border-radius: 3px;
+  padding: 0 20px;
+  text-decoration: none;
+  color: $bright-text-color;
+  height: 40px;
+  line-height: 42px;
+}
+
+.restart-button {
+  @include button;
+  display: block;
+  width: 100px;
+  margin: 10px auto 10px auto;
+  text-align: center;
+}
+
 // Game field mixin used to render CSS at different width
 @mixin game-field {
   .game-container {
-    margin-top: 40px;
     position: relative;
     padding: $grid-spacing;
 
@@ -190,16 +224,13 @@
       }
 
       a {
-        display: inline-block;
-        background: darken($game-container-background, 20%);
-        border-radius: 3px;
-        padding: 0 20px;
-        text-decoration: none;
-        color: $bright-text-color;
-        height: 40px;
-        line-height: 42px;
+        @include button;
         margin-left: 9px;
         // margin-top: 59px;
+
+	&.keep-playing-button {
+	  display: none;
+	}
       }
 
       @include animation(fade-in 800ms ease $transition-speed * 12);
@@ -208,6 +239,10 @@
       &.game-won {
         background: rgba($tile-gold-color, .5);
         color: $bright-text-color;
+
+	a.keep-playing-button {
+	  display: inline-block;
+	}
       }
 
       &.game-won, &.game-over {
@@ -256,17 +291,19 @@
   }
 
   .tile {
-    width: $tile-size;
-    height: $tile-size;
-    line-height: $tile-size + 10px;
+    &, .tile-inner {
+      width: ceil($tile-size);
+      height: ceil($tile-size);
+      line-height: $tile-size + 10px;
+    }
 
     // Build position classes
     @for $x from 1 through $grid-row-cells {
       @for $y from 1 through $grid-row-cells {
         &.tile-position-#{$x}-#{$y} {
-          position: absolute;
-          left: round(($tile-size + $grid-spacing) * ($x - 1));
-          top: round(($tile-size + $grid-spacing) * ($y - 1));
+          $xPos: floor(($tile-size + $grid-spacing) * ($x - 1));
+          $yPos: floor(($tile-size + $grid-spacing) * ($y - 1));
+          @include transform(translate($xPos, $yPos));
         }
       }
     }
@@ -277,17 +314,24 @@
 @include game-field;
 
 .tile {
-  border-radius: $tile-border-radius;
-
-  background: $tile-color;
-  text-align: center;
-  font-weight: bold;
-  z-index: 10;
-
-  font-size: 55px;
-
+  position: absolute; // Makes transforms relative to the top-left corner
+
+  .tile-inner {
+    border-radius: $tile-border-radius;
+
+    background: $tile-color;
+    text-align: center;
+    font-weight: bold;
+    z-index: 10;
+
+    font-size: 55px;
+  }
+
+  // Movement transition
   @include transition($transition-speed ease-in-out);
-  @include transition-property(top, left);
+  -webkit-transition-property: -webkit-transform;
+  -moz-transition-property: -moz-transform;
+  transition-property: transform;
 
   $base: 2;
   $exponent: 1;
@@ -310,7 +354,7 @@
   @while $exponent <= $limit {
     $power: pow($base, $exponent);
 
-    &.tile-#{$power} {
+    &.tile-#{$power} .tile-inner {
       // Calculate base background color
       $gold-percent: ($exponent - 1) / ($limit - 1) * 100;
       $mixed-background: mix($tile-gold-color, $tile-color, $gold-percent);
@@ -344,62 +388,69 @@
         font-size: 45px;
 
         // Media queries placed here to avoid carrying over the rest of the logic
-        @include smaller(480px) {
+        @include smaller($mobile-threshold) {
           font-size: 25px;
         }
       } @else if $power >= 1000 {
         font-size: 35px;
 
-        @include smaller(480px) {
+        @include smaller($mobile-threshold) {
           font-size: 15px;
         }
       }
     }
 
     $exponent: $exponent + 1;
+  }
+
+  // Super tiles (above 2048)
+  &.tile-super .tile-inner {
+    color: $bright-text-color;
+    background: mix(#333, $tile-gold-color, 95%);
+
+    font-size: 30px;
+
+    @include smaller($mobile-threshold) {
+      font-size: 10px;
+    }
   }
 }
 
 @include keyframes(appear) {
   0% {
     opacity: 0;
-    -webkit-transform: scale(0);
-    -moz-transform: scale(0);
+    @include transform(scale(0));
   }
 
   100% {
     opacity: 1;
-    -webkit-transform: scale(1);
-    -moz-transform: scale(1);
-  }
-}
-
-.tile-new {
+    @include transform(scale(1));
+  }
+}
+
+.tile-new .tile-inner {
   @include animation(appear 200ms ease $transition-speed);
-  @include animation-fill-mode(both);
+  @include animation-fill-mode(backwards);
 }
 
 @include keyframes(pop) {
   0% {
-    -webkit-transform: scale(0);
-    -moz-transform: scale(0);
+    @include transform(scale(0));
   }
 
   50% {
-    -webkit-transform: scale(1.2);
-    -moz-transform: scale(1.2);
+    @include transform(scale(1.2));
   }
 
   100% {
-    -webkit-transform: scale(1);
-    -moz-transform: scale(1);
-  }
-}
-
-.tile-merged {
+    @include transform(scale(1));
+  }
+}
+
+.tile-merged .tile-inner {
   z-index: 20;
   @include animation(pop 200ms ease $transition-speed);
-  @include animation-fill-mode(both);
+  @include animation-fill-mode(backwards);
 }
 
 .game-intro {
@@ -410,7 +461,7 @@
   margin-top: 50px;
 }
 
-@include smaller(480px) {
+@include smaller($mobile-threshold) {
   // Redefine variables for smaller screens
   $field-width: 280px;
   $grid-spacing: 10px;
@@ -428,7 +479,8 @@
   }
 
   h1.title {
-    font-size: 50px;
+    font-size: 27px;
+    margin-top: 15px;
   }
 
   .container {
@@ -436,8 +488,10 @@
     margin: 0 auto;
   }
 
-  .score-container {
+  .score-container, .best-container {
     margin-top: 0;
+    padding: 15px 10px;
+    min-width: 40px;
   }
 
   .heading {
@@ -452,7 +506,7 @@
   }
 
   // Rest of the font-size adjustments in the tile class
-  .tile {
+  .tile .tile-inner {
     font-size: 35px;
   }
 

comments