diff --git a/README.md b/README.md
index 96a85a75ecf3c8eb87c5f9bff1d6127bc366f733..19c6d0e011ecbbf40127c745354554645736a910 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,9 @@ Many thanks to [rayhaanj](https://github.com/rayhaanj), [Mechazawa](https://gith
### Screenshot
-[](http://pictures.gabrielecirulli.com/2048-20140309-234100.png)
+
@@ -109,7 +113,7 @@
-
+
diff --git a/js/application.js b/js/application.js
index a4d310a2579bf9940e3844e9e169d65b705865fe..2c1108e757a0e49af7b9ee48dcc45bd8e61cb806 100644
--- a/js/application.js
+++ b/js/application.js
@@ -1,4 +1,4 @@
// Wait till the browser is ready to render the game (avoids glitches)
window.requestAnimationFrame(function () {
- new GameManager(4, KeyboardInputManager, HTMLActuator, LocalScoreManager);
+ new GameManager(4, KeyboardInputManager, HTMLActuator, LocalStorageManager);
});
diff --git a/js/game_manager.js b/js/game_manager.js
index b36aea3122986eb8708de896d014d7ae613e3c05..f60fd2d2420d7577f0005b7dd22eec8c5a16756c 100644
--- a/js/game_manager.js
+++ b/js/game_manager.js
@@ -1,10 +1,10 @@
-function GameManager(size, InputManager, Actuator, ScoreManager) {
- this.size = size; // Size of the grid
- this.inputManager = new InputManager;
- this.scoreManager = new ScoreManager;
- this.actuator = new 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.startTiles = 2;
this.inputManager.on("move", this.move.bind(this));
this.inputManager.on("restart", this.restart.bind(this));
@@ -15,6 +15,7 @@ function GameManager(size, InputManager, Actuator, ScoreManager) {
// Restart the game
GameManager.prototype.restart = function () {
+ this.storageManager.clearGameState();
this.actuator.continue();
this.setup();
};
@@ -35,15 +36,25 @@ GameManager.prototype.isGameTerminated = function () {
// 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 previousState = this.storageManager.getGameState();
+
+ if (previousState) {
+ this.grid = new Grid(previousState.grid.size,
+ previousState.grid.cells); // Reload grid
+ this.score = previousState.score;
+ this.over = previousState.over;
+ this.won = previousState.won;
+ this.keepPlaying = previousState.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,20 +79,32 @@ GameManager.prototype.addRandomTile = function () {
// 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.serialize());
+
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.serialize = function () {
+ return {
+ grid: this.grid.serialize(),
+ score: this.score,
+ over: this.over,
+ won: this.won,
+ keepPlaying: this.keepPlaying
+ };
+};
+
// Save all tile positions and remove merger info
GameManager.prototype.prepareTiles = function () {
this.grid.eachCell(function (x, y, tile) {
diff --git a/js/grid.js b/js/grid.js
index 05fe057805028767f6afd3036e16c8fb69a3d36b..29f0821e505b0440db8ba23c6ae88f4b5f44180a 100644
--- a/js/grid.js
+++ b/js/grid.js
@@ -1,20 +1,36 @@
-function Grid(size) {
+function Grid(size, previousState) {
this.size = size;
-
- this.cells = [];
-
- this.build();
+ this.cells = previousState ? this.fromState(previousState) : this.empty();
}
// Build a grid of the specified size
-Grid.prototype.build = function () {
+Grid.prototype.empty = 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.fromState = 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 tile = state[x][y];
+ row.push(tile ? new Tile(tile.position, tile.value) : null);
+ }
+ }
+
+ return cells;
};
// Find the first available random position
@@ -82,3 +98,20 @@ Grid.prototype.withinBounds = function (position) {
return position.x >= 0 && position.x < this.size &&
position.y >= 0 && position.y < this.size;
};
+
+Grid.prototype.serialize = 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].serialize() : null);
+ }
+ }
+
+ return {
+ size: this.size,
+ cells: cellState
+ };
+};
diff --git a/js/keyboard_input_manager.js b/js/keyboard_input_manager.js
index a29744cc0ae26c8b87ba95bafe2a3c6220c23365..32a177a47dc5452c979cdd0e14d1a2a188e779a0 100644
--- a/js/keyboard_input_manager.js
+++ b/js/keyboard_input_manager.js
@@ -39,16 +39,17 @@ KeyboardInputManager.prototype.listen = function () {
39: 1, // Right
40: 2, // Down
37: 3, // Left
- 75: 0, // vim keybindings
- 76: 1,
- 74: 2,
- 72: 3,
+ 75: 0, // Vim up
+ 76: 1, // Vim right
+ 74: 2, // Vim down
+ 72: 3, // Vim left
87: 0, // W
68: 1, // D
83: 2, // S
65: 3 // A
};
+ // Respond to direction keys
document.addEventListener("keydown", function (event) {
var modifiers = event.altKey || event.ctrlKey || event.metaKey ||
event.shiftKey;
@@ -59,34 +60,37 @@ KeyboardInputManager.prototype.listen = function () {
event.preventDefault();
self.emit("move", mapped);
}
+ }
- if (event.which === 32) self.restart.bind(self)(event);
+ // R key restarts the game
+ if (!modifiers && event.which === 82) {
+ self.restart.call(self, event);
}
});
- var retry = document.querySelector(".retry-button");
- retry.addEventListener("click", this.restart.bind(this));
- retry.addEventListener(this.eventTouchend, 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));
+ // Respond to button presses
+ this.bindButtonPress(".retry-button", this.restart);
+ this.bindButtonPress(".restart-button", this.restart);
+ this.bindButtonPress(".keep-playing-button", this.keepPlaying);
- // Listen to swipe events
+ // Respond to swipe events
var touchStartClientX, touchStartClientY;
var gameContainer = document.getElementsByClassName("game-container")[0];
gameContainer.addEventListener(this.eventTouchstart, function (event) {
- if (( !window.navigator.msPointerEnabled && event.touches.length > 1) || event.targetTouches > 1) return;
-
- if(window.navigator.msPointerEnabled){
- touchStartClientX = event.pageX;
- touchStartClientY = event.pageY;
+ if ((!window.navigator.msPointerEnabled && event.touches.length > 1) ||
+ event.targetTouches > 1) {
+ return; // Ignore if touching with more than 1 finger
+ }
+
+ if (window.navigator.msPointerEnabled) {
+ touchStartClientX = event.pageX;
+ touchStartClientY = event.pageY;
} else {
- touchStartClientX = event.touches[0].clientX;
- touchStartClientY = event.touches[0].clientY;
+ touchStartClientX = event.touches[0].clientX;
+ touchStartClientY = event.touches[0].clientY;
}
-
+
event.preventDefault();
});
@@ -95,15 +99,19 @@ KeyboardInputManager.prototype.listen = function () {
});
gameContainer.addEventListener(this.eventTouchend, function (event) {
- if (( !window.navigator.msPointerEnabled && event.touches.length > 0) || event.targetTouches > 0) return;
+ if ((!window.navigator.msPointerEnabled && event.touches.length > 0) ||
+ event.targetTouches > 0) {
+ return; // Ignore if still touching with one or more fingers
+ }
var touchEndClientX, touchEndClientY;
- if(window.navigator.msPointerEnabled){
- touchEndClientX = event.pageX;
- touchEndClientY = event.pageY;
+
+ if (window.navigator.msPointerEnabled) {
+ touchEndClientX = event.pageX;
+ touchEndClientY = event.pageY;
} else {
- touchEndClientX = event.changedTouches[0].clientX;
- touchEndClientY = event.changedTouches[0].clientY;
+ touchEndClientX = event.changedTouches[0].clientX;
+ touchEndClientY = event.changedTouches[0].clientY;
}
var dx = touchEndClientX - touchStartClientX;
@@ -128,3 +136,9 @@ KeyboardInputManager.prototype.keepPlaying = function (event) {
event.preventDefault();
this.emit("keepPlaying");
};
+
+KeyboardInputManager.prototype.bindButtonPress = function (selector, fn) {
+ var button = document.querySelector(selector);
+ button.addEventListener("click", fn.bind(this));
+ button.addEventListener(this.eventTouchend, fn.bind(this));
+};
diff --git a/js/local_score_manager.js b/js/local_score_manager.js
deleted file mode 100644
index ec4575dafa61af1b52de9d435e1ae40980a43d97..0000000000000000000000000000000000000000
--- a/js/local_score_manager.js
+++ /dev/null
@@ -1,48 +0,0 @@
-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.key = "bestScore";
-
- 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.get = function () {
- return this.storage.getItem(this.key) || 0;
-};
-
-LocalScoreManager.prototype.set = function (score) {
- this.storage.setItem(this.key, score);
-};
-
diff --git a/js/local_storage_manager.js b/js/local_storage_manager.js
new file mode 100644
index 0000000000000000000000000000000000000000..776e94b1abea8afecbe41daafbb23c8d74a0f328
--- /dev/null
+++ b/js/local_storage_manager.js
@@ -0,0 +1,63 @@
+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 LocalStorageManager() {
+ this.bestScoreKey = "bestScore";
+ this.gameStateKey = "gameState";
+
+ var supported = this.localStorageSupported();
+ this.storage = supported ? window.localStorage : window.fakeStorage;
+}
+
+LocalStorageManager.prototype.localStorageSupported = function () {
+ var testKey = "test";
+ var storage = window.localStorage;
+
+ try {
+ storage.setItem(testKey, "1");
+ storage.removeItem(testKey);
+ return true;
+ } catch (error) {
+ return false;
+ }
+};
+
+// Best score getters/setters
+LocalStorageManager.prototype.getBestScore = function () {
+ return this.storage.getItem(this.bestScoreKey) || 0;
+};
+
+LocalStorageManager.prototype.setBestScore = function (score) {
+ this.storage.setItem(this.bestScoreKey, score);
+};
+
+// Game state getters/setters and clearing
+LocalStorageManager.prototype.getGameState = function () {
+ var stateJSON = this.storage.getItem(this.gameStateKey);
+ return stateJSON ? JSON.parse(stateJSON) : null;
+};
+
+LocalStorageManager.prototype.setGameState = function (gameState) {
+ this.storage.setItem(this.gameStateKey, JSON.stringify(gameState));
+};
+
+LocalStorageManager.prototype.clearGameState = function () {
+ this.storage.removeItem(this.gameStateKey);
+};
diff --git a/js/tile.js b/js/tile.js
index de083331bb8353637e81a67304e2e2f8170a7aae..92a670a5a60507bae99f333f0d3a9e23706f629f 100644
--- a/js/tile.js
+++ b/js/tile.js
@@ -15,3 +15,13 @@ Tile.prototype.updatePosition = function (position) {
this.x = position.x;
this.y = position.y;
};
+
+Tile.prototype.serialize = function () {
+ return {
+ position: {
+ x: this.x,
+ y: this.y
+ },
+ value: this.value
+ };
+};
diff --git a/style/helpers.scss b/style/helpers.scss
index 82f5420e4c9ad827fca163e62c470d377ae013fe..3786bc09ae2c21b9c4c6612da26a216fced30c69 100644
--- a/style/helpers.scss
+++ b/style/helpers.scss
@@ -77,3 +77,12 @@
-moz-appearance: $args;
appearance: $args;
}
+
+// Clearfix
+@mixin clearfix {
+ &:after {
+ content: "";
+ display: block;
+ clear: both;
+ }
+}
diff --git a/style/main.css b/style/main.css
index c62cfb4f4c945b3f62d1954d68b19264328fd1c8..41557e96ba8d8bd5a4809751ccfdeb9ab47ac6d1 100644
--- a/style/main.css
+++ b/style/main.css
@@ -30,7 +30,6 @@ h1.title {
100% {
top: -50px;
opacity: 0; } }
-
@-moz-keyframes move-up {
0% {
top: 25px;
@@ -39,7 +38,6 @@ h1.title {
100% {
top: -50px;
opacity: 0; } }
-
@keyframes move-up {
0% {
top: 25px;
@@ -48,7 +46,6 @@ h1.title {
100% {
top: -50px;
opacity: 0; } }
-
.scores-container {
float: right;
text-align: right; }
@@ -128,21 +125,18 @@ hr {
100% {
opacity: 1; } }
-
@-moz-keyframes fade-in {
0% {
opacity: 0; }
100% {
opacity: 1; } }
-
@keyframes fade-in {
0% {
opacity: 0; }
100% {
opacity: 1; } }
-
.game-container {
margin-top: 40px;
position: relative;
@@ -397,7 +391,6 @@ hr {
-webkit-transform: scale(1);
-moz-transform: scale(1);
transform: scale(1); } }
-
@-moz-keyframes appear {
0% {
opacity: 0;
@@ -410,7 +403,6 @@ hr {
-webkit-transform: scale(1);
-moz-transform: scale(1);
transform: scale(1); } }
-
@keyframes appear {
0% {
opacity: 0;
@@ -423,7 +415,6 @@ hr {
-webkit-transform: scale(1);
-moz-transform: scale(1);
transform: scale(1); } }
-
.tile-new .tile-inner {
-webkit-animation: appear 200ms ease 100ms;
-moz-animation: appear 200ms ease 100ms;
@@ -447,7 +438,6 @@ hr {
-webkit-transform: scale(1);
-moz-transform: scale(1);
transform: scale(1); } }
-
@-moz-keyframes pop {
0% {
-webkit-transform: scale(0);
@@ -463,7 +453,6 @@ hr {
-webkit-transform: scale(1);
-moz-transform: scale(1);
transform: scale(1); } }
-
@keyframes pop {
0% {
-webkit-transform: scale(0);
@@ -479,7 +468,6 @@ hr {
-webkit-transform: scale(1);
-moz-transform: scale(1);
transform: scale(1); } }
-
.tile-merged .tile-inner {
z-index: 20;
-webkit-animation: pop 200ms ease 100ms;
@@ -489,8 +477,27 @@ hr {
-moz-animation-fill-mode: backwards;
animation-fill-mode: backwards; }
+.above-game:after {
+ content: "";
+ display: block;
+ clear: both; }
+
.game-intro {
- margin-bottom: 0; }
+ float: left;
+ line-height: 42px; }
+
+.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;
+ text-align: center;
+ float: right; }
.game-explanation {
margin-top: 50px; }
@@ -526,6 +533,19 @@ hr {
.heading {
margin-bottom: 10px; }
+ .game-intro {
+ width: 55%;
+ display: block;
+ box-sizing: border-box;
+ line-height: 1.65; }
+
+ .restart-button {
+ width: 42%;
+ padding: 0;
+ display: block;
+ box-sizing: border-box;
+ margin-top: 2px; }
+
.game-container {
margin-top: 40px;
position: relative;
diff --git a/style/main.scss b/style/main.scss
index 3c5dd08d1b86f15f5c825fa0f739d6a22cab91ab..753e43cee5f405a506b5a55bf5e8fd15bbb4544e 100644
--- a/style/main.scss
+++ b/style/main.scss
@@ -34,10 +34,8 @@ body {
margin: 80px 0;
}
-.heading:after {
- content: "";
- display: block;
- clear: both;
+.heading {
+ @include clearfix;
}
h1.title {
@@ -453,8 +451,24 @@ hr {
@include animation-fill-mode(backwards);
}
+.above-game {
+ @include clearfix;
+ // margin-bottom: 10px;
+}
+
.game-intro {
- margin-bottom: 0;
+ float: left;
+ line-height: 42px;
+ // margin-bottom: 0;
+}
+
+.restart-button {
+ @include button;
+ display: block;
+ // width: 100px;
+ // margin: 10px auto 10px auto;
+ text-align: center;
+ float: right;
}
.game-explanation {
@@ -508,6 +522,22 @@ hr {
margin-bottom: 10px;
}
+ // Show intro and restart button side by side
+ .game-intro {
+ width: 55%;
+ display: block;
+ box-sizing: border-box;
+ line-height: 1.65;
+ }
+
+ .restart-button {
+ width: 42%;
+ padding: 0;
+ display: block;
+ box-sizing: border-box;
+ margin-top: 2px;
+ }
+
// Render the game field at the right width
@include game-field;