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
This commit is contained in:
Mark Frederiksen
2014-03-18 14:48:55 -04:00
parent 01264c3252
commit 02a24c0610
8 changed files with 125 additions and 28 deletions
+1 -1
View File
@@ -23,7 +23,7 @@
</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>
<a class="restart-button">New Game</a>
<div class="game-container"> <div class="game-container">
<div class="game-message"> <div class="game-message">
<p></p> <p></p>
+35 -12
View File
@@ -1,7 +1,7 @@
function GameManager(size, InputManager, Actuator, ScoreManager) { function GameManager(size, InputManager, Actuator, StorageManager) {
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.storageManager = new StorageManager;
this.actuator = new Actuator; this.actuator = new Actuator;
this.startTiles = 2; this.startTiles = 2;
@@ -15,6 +15,7 @@ function GameManager(size, InputManager, Actuator, ScoreManager) {
// Restart the game // Restart the game
GameManager.prototype.restart = function () { GameManager.prototype.restart = function () {
this.storageManager.clearGameState();
this.actuator.continue(); this.actuator.continue();
this.setup(); this.setup();
}; };
@@ -35,15 +36,24 @@ GameManager.prototype.isGameTerminated = function () {
// Set up the game // Set up the game
GameManager.prototype.setup = function () { GameManager.prototype.setup = function () {
this.grid = new Grid(this.size); var previousGameState = this.storageManager.getGameState();
this.score = 0; if (previousGameState) {
this.over = false; this.grid = new Grid(previousGameState.grid.size, previousGameState.grid.cells);
this.won = false; this.score = previousGameState.score;
this.keepPlaying = false; 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 // Add the initial tiles
this.addStartTiles(); this.addStartTiles();
}
// Update the actuator // Update the actuator
this.actuate(); this.actuate();
@@ -68,20 +78,33 @@ GameManager.prototype.addRandomTile = function () {
// 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.storageManager.getBestScore() < this.score) {
this.scoreManager.set(this.score); this.storageManager.setBestScore(this.score);
} }
this.storageManager.setGameState(this.serializeGameState());
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.storageManager.getBestScore(),
terminated: this.isGameTerminated() 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 // 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) {
+35 -7
View File
@@ -1,20 +1,32 @@
function Grid(size) { function Grid(size, previousCellState) {
this.size = size; this.size = size;
this.cells = previousCellState ? this.buildFromPreviousState(previousCellState) : this.buildNew();
this.cells = [];
this.build();
} }
// Build a grid of the specified size // 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++) { 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++) { for (var y = 0; y < this.size; y++) {
row.push(null); 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 // Find the first available random position
@@ -82,3 +94,19 @@ Grid.prototype.withinBounds = function (position) {
return position.x >= 0 && position.x < this.size && return position.x >= 0 && position.x < this.size &&
position.y >= 0 && position.y < this.size; 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
}
};
+4
View File
@@ -57,6 +57,10 @@ KeyboardInputManager.prototype.listen = function () {
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 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"); var keepPlaying = document.querySelector(".keep-playing-button");
keepPlaying.addEventListener("click", this.keepPlaying.bind(this)); keepPlaying.addEventListener("click", this.keepPlaying.bind(this));
keepPlaying.addEventListener("touchend", this.keepPlaying.bind(this)); keepPlaying.addEventListener("touchend", this.keepPlaying.bind(this));
+18 -5
View File
@@ -19,7 +19,8 @@ window.fakeStorage = {
}; };
function LocalScoreManager() { function LocalScoreManager() {
this.key = "bestScore"; this.bestScoreKey = "bestScore";
this.gameStateKey = "gameState";
var supported = this.localStorageSupported(); var supported = this.localStorageSupported();
this.storage = supported ? window.localStorage : window.fakeStorage; this.storage = supported ? window.localStorage : window.fakeStorage;
@@ -38,11 +39,23 @@ LocalScoreManager.prototype.localStorageSupported = function () {
} }
}; };
LocalScoreManager.prototype.get = function () { LocalScoreManager.prototype.getBestScore = function () {
return this.storage.getItem(this.key) || 0; return this.storage.getItem(this.bestScoreKey) || 0;
}; };
LocalScoreManager.prototype.set = function (score) { LocalScoreManager.prototype.setBestScore = function (score) {
this.storage.setItem(this.key, 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);
};
+10
View File
@@ -15,3 +15,13 @@ Tile.prototype.updatePosition = function (position) {
this.x = position.x; this.x = position.x;
this.y = position.y; this.y = position.y;
}; };
Tile.prototype.tileState = function () {
return {
position: {
x: this.x,
y: this.y
},
value: this.value
};
}
+14 -2
View File
@@ -143,8 +143,21 @@ hr {
100% { 100% {
opacity: 1; } } 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 { .game-container {
margin-top: 40px;
position: relative; position: relative;
padding: 15px; padding: 15px;
cursor: default; cursor: default;
@@ -515,7 +528,6 @@ hr {
margin-bottom: 10px; } margin-bottom: 10px; }
.game-container { .game-container {
margin-top: 40px;
position: relative; position: relative;
padding: 10px; padding: 10px;
cursor: default; cursor: default;
+8 -1
View File
@@ -168,10 +168,17 @@ hr {
line-height: 42px; 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 // Game field mixin used to render CSS at different width
@mixin game-field { @mixin game-field {
.game-container { .game-container {
margin-top: 40px;
position: relative; position: relative;
padding: $grid-spacing; padding: $grid-spacing;