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 -[![Screenshot](http://pictures.gabrielecirulli.com/2048-20140309-234100.png)](http://pictures.gabrielecirulli.com/2048-20140309-234100.png) +

+ Screenshot +

That screenshot is fake, by the way. I never reached 2048 :smile: diff --git a/cache.appcache b/cache.appcache index f36e8a420b8149c4eae0c965ecf68c3a560e7bee..d3b975dda97458236440311f600401488e91cbc5 100644 --- a/cache.appcache +++ b/cache.appcache @@ -3,7 +3,7 @@ CACHE MANIFEST # Adds the ability to play the game online. # The following comment needs to be updated whenever a change is made. # Run `rake appcache:update` to do so -# Updated: 2014-03-21T13:04:30+01:00 +# Updated: 2014-03-22T17:11:53+01:00 # Main page index.html @@ -36,7 +36,7 @@ js/game_manager.js js/grid.js js/html_actuator.js js/keyboard_input_manager.js -js/local_score_manager.js +js/local_storage_manager.js js/tile.js favicon.ico diff --git a/index.html b/index.html index b1a8ccef0c8e449b1badda886fd4e6c73c2e7278..6166058ddd0897132375bbe8d95d05a7a4bc927b 100644 --- a/index.html +++ b/index.html @@ -28,7 +28,11 @@
0
-

Join the numbers and get to the 2048 tile!

+ +
+

Join the numbers and get to the 2048 tile!

+ New Game +
@@ -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;