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/index.html -> file:b/index.html
--- a/index.html
+++ b/index.html
@@ -23,7 +23,7 @@
       </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>

--- a/js/game_manager.js
+++ b/js/game_manager.js
@@ -1,7 +1,7 @@
-function GameManager(size, InputManager, Actuator, ScoreManager) {
+function GameManager(size, InputManager, Actuator, StorageManager) {
   this.size         = size; // Size of the grid
   this.inputManager = new InputManager;
-  this.scoreManager = new ScoreManager;
+  this.storageManager = new StorageManager;
   this.actuator     = new Actuator;
 
   this.startTiles   = 2;
@@ -15,6 +15,7 @@
 
 // Restart the game
 GameManager.prototype.restart = function () {
+  this.storageManager.clearGameState();
   this.actuator.continue();
   this.setup();
 };
@@ -35,15 +36,24 @@
 
 // Set up the game
 GameManager.prototype.setup = function () {
-  this.grid        = new Grid(this.size);
-
-  this.score       = 0;
-  this.over        = false;
-  this.won         = false;
-  this.keepPlaying = 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();
@@ -68,19 +78,32 @@
 
 // Sends the updated grid to the actuator
 GameManager.prototype.actuate = function () {
-  if (this.scoreManager.get() < this.score) {
-    this.scoreManager.set(this.score);
-  }
+  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,
-    bestScore:  this.scoreManager.get(),
+    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 () {

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
+  }
+};
+

--- a/js/keyboard_input_manager.js
+++ b/js/keyboard_input_manager.js
@@ -57,6 +57,10 @@
   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));

--- a/js/local_score_manager.js
+++ b/js/local_score_manager.js
@@ -19,7 +19,8 @@
 };
 
 function LocalScoreManager() {
-  this.key     = "bestScore";
+  this.bestScoreKey     = "bestScore";
+  this.gameStateKey     = "gameState";
 
   var supported = this.localStorageSupported();
   this.storage = supported ? window.localStorage : window.fakeStorage;
@@ -38,12 +39,24 @@
   }
 };
 
-LocalScoreManager.prototype.get = function () {
-  return this.storage.getItem(this.key) || 0;
+LocalScoreManager.prototype.getBestScore = function () {
+  return this.storage.getItem(this.bestScoreKey) || 0;
 };
 
-LocalScoreManager.prototype.set = function (score) {
-  this.storage.setItem(this.key, score);
+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
+    };
+}

--- a/style/main.css
+++ b/style/main.css
@@ -143,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;
@@ -515,7 +528,6 @@
     margin-bottom: 10px; }
 
   .game-container {
-    margin-top: 40px;
     position: relative;
     padding: 10px;
     cursor: default;

--- a/style/main.scss
+++ b/style/main.scss
@@ -168,10 +168,17 @@
   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;
 

comments