├── favicon.ico ├── meta ├── apple-touch-icon.png ├── apple-touch-startup-image-640x1096.png └── apple-touch-startup-image-640x920.png ├── style ├── fonts │ ├── ClearSans-Bold-webfont.eot │ ├── ClearSans-Bold-webfont.eot? │ ├── ClearSans-Bold-webfont.woff │ ├── ClearSans-Light-webfont.eot │ ├── ClearSans-Light-webfont.eot? │ ├── ClearSans-Light-webfont.woff │ ├── ClearSans-Regular-webfont.eot │ ├── ClearSans-Regular-webfont.eot? │ ├── ClearSans-Regular-webfont.woff │ ├── clear-sans.css │ ├── ClearSans-Bold-webfont.svg │ └── ClearSans-Regular-webfont.svg └── main.css ├── js ├── application.js ├── bind_polyfill.js ├── tile.js ├── animframe_polyfill.js ├── local_storage_manager.js ├── classlist_polyfill.js ├── grid.js ├── html_actuator.js ├── keyboard_input_manager.js └── game_manager.js └── index.html /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browseDNS/2048/master/favicon.ico -------------------------------------------------------------------------------- /meta/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browseDNS/2048/master/meta/apple-touch-icon.png -------------------------------------------------------------------------------- /style/fonts/ClearSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browseDNS/2048/master/style/fonts/ClearSans-Bold-webfont.eot -------------------------------------------------------------------------------- /style/fonts/ClearSans-Bold-webfont.eot?: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browseDNS/2048/master/style/fonts/ClearSans-Bold-webfont.eot? -------------------------------------------------------------------------------- /style/fonts/ClearSans-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browseDNS/2048/master/style/fonts/ClearSans-Bold-webfont.woff -------------------------------------------------------------------------------- /style/fonts/ClearSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browseDNS/2048/master/style/fonts/ClearSans-Light-webfont.eot -------------------------------------------------------------------------------- /style/fonts/ClearSans-Light-webfont.eot?: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browseDNS/2048/master/style/fonts/ClearSans-Light-webfont.eot? -------------------------------------------------------------------------------- /style/fonts/ClearSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browseDNS/2048/master/style/fonts/ClearSans-Light-webfont.woff -------------------------------------------------------------------------------- /meta/apple-touch-startup-image-640x1096.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browseDNS/2048/master/meta/apple-touch-startup-image-640x1096.png -------------------------------------------------------------------------------- /meta/apple-touch-startup-image-640x920.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browseDNS/2048/master/meta/apple-touch-startup-image-640x920.png -------------------------------------------------------------------------------- /style/fonts/ClearSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browseDNS/2048/master/style/fonts/ClearSans-Regular-webfont.eot -------------------------------------------------------------------------------- /style/fonts/ClearSans-Regular-webfont.eot?: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browseDNS/2048/master/style/fonts/ClearSans-Regular-webfont.eot? -------------------------------------------------------------------------------- /style/fonts/ClearSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/browseDNS/2048/master/style/fonts/ClearSans-Regular-webfont.woff -------------------------------------------------------------------------------- /js/application.js: -------------------------------------------------------------------------------- 1 | // Wait till the browser is ready to render the game (avoids glitches) 2 | window.requestAnimationFrame(function () { 3 | new GameManager(4, KeyboardInputManager, HTMLActuator, LocalStorageManager); 4 | }); 5 | -------------------------------------------------------------------------------- /js/bind_polyfill.js: -------------------------------------------------------------------------------- 1 | Function.prototype.bind = Function.prototype.bind || function (target) { 2 | var self = this; 3 | return function (args) { 4 | if (!(args instanceof Array)) { 5 | args = [args]; 6 | } 7 | self.apply(target, args); 8 | }; 9 | }; 10 | -------------------------------------------------------------------------------- /js/tile.js: -------------------------------------------------------------------------------- 1 | function Tile(position, value) { 2 | this.x = position.x; 3 | this.y = position.y; 4 | this.value = value || 2; 5 | 6 | this.previousPosition = null; 7 | this.mergedFrom = null; // Tracks tiles that merged together 8 | } 9 | 10 | Tile.prototype.savePosition = function () { 11 | this.previousPosition = { x: this.x, y: this.y }; 12 | }; 13 | 14 | Tile.prototype.updatePosition = function (position) { 15 | this.x = position.x; 16 | this.y = position.y; 17 | }; 18 | 19 | Tile.prototype.serialize = function () { 20 | return { 21 | position: { 22 | x: this.x, 23 | y: this.y 24 | }, 25 | value: this.value 26 | }; 27 | }; 28 | -------------------------------------------------------------------------------- /js/animframe_polyfill.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | var lastTime = 0; 3 | var vendors = ['webkit', 'moz']; 4 | for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) { 5 | window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame']; 6 | window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || 7 | window[vendors[x] + 'CancelRequestAnimationFrame']; 8 | } 9 | 10 | if (!window.requestAnimationFrame) { 11 | window.requestAnimationFrame = function (callback) { 12 | var currTime = new Date().getTime(); 13 | var timeToCall = Math.max(0, 16 - (currTime - lastTime)); 14 | var id = window.setTimeout(function () { 15 | callback(currTime + timeToCall); 16 | }, 17 | timeToCall); 18 | lastTime = currTime + timeToCall; 19 | return id; 20 | }; 21 | } 22 | 23 | if (!window.cancelAnimationFrame) { 24 | window.cancelAnimationFrame = function (id) { 25 | clearTimeout(id); 26 | }; 27 | } 28 | }()); 29 | -------------------------------------------------------------------------------- /style/fonts/clear-sans.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Clear Sans"; 3 | src: url("ClearSans-Light-webfont.eot"); 4 | src: url("ClearSans-Light-webfont.eot?") format("embedded-opentype"), 5 | url("ClearSans-Light-webfont.svg") format("svg"), 6 | url("ClearSans-Light-webfont.woff") format("woff"); 7 | font-weight: 200; 8 | font-style: normal; 9 | } 10 | 11 | @font-face { 12 | font-family: "Clear Sans"; 13 | src: url("ClearSans-Regular-webfont.eot"); 14 | src: url("ClearSans-Regular-webfont.eot?") format("embedded-opentype"), 15 | url("ClearSans-Regular-webfont.svg") format("svg"), 16 | url("ClearSans-Regular-webfont.woff") format("woff"); 17 | font-weight: normal; 18 | font-style: normal; 19 | } 20 | 21 | @font-face { 22 | font-family: "Clear Sans"; 23 | src: url("ClearSans-Bold-webfont.eot"); 24 | src: url("ClearSans-Bold-webfont.eot?") format("embedded-opentype"), 25 | url("ClearSans-Bold-webfont.svg") format("svg"), 26 | url("ClearSans-Bold-webfont.woff") format("woff"); 27 | font-weight: 700; 28 | font-style: normal; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /js/local_storage_manager.js: -------------------------------------------------------------------------------- 1 | window.fakeStorage = { 2 | _data: {}, 3 | 4 | setItem: function (id, val) { 5 | return this._data[id] = String(val); 6 | }, 7 | 8 | getItem: function (id) { 9 | return this._data.hasOwnProperty(id) ? this._data[id] : undefined; 10 | }, 11 | 12 | removeItem: function (id) { 13 | return delete this._data[id]; 14 | }, 15 | 16 | clear: function () { 17 | return this._data = {}; 18 | } 19 | }; 20 | 21 | function LocalStorageManager() { 22 | this.bestScoreKey = "bestScore"; 23 | this.gameStateKey = "gameState"; 24 | 25 | var supported = this.localStorageSupported(); 26 | this.storage = supported ? window.localStorage : window.fakeStorage; 27 | } 28 | 29 | LocalStorageManager.prototype.localStorageSupported = function () { 30 | var testKey = "test"; 31 | 32 | try { 33 | var storage = window.localStorage; 34 | storage.setItem(testKey, "1"); 35 | storage.removeItem(testKey); 36 | return true; 37 | } catch (error) { 38 | return false; 39 | } 40 | }; 41 | 42 | // Best score getters/setters 43 | LocalStorageManager.prototype.getBestScore = function () { 44 | return this.storage.getItem(this.bestScoreKey) || 0; 45 | }; 46 | 47 | LocalStorageManager.prototype.setBestScore = function (score) { 48 | this.storage.setItem(this.bestScoreKey, score); 49 | }; 50 | 51 | // Game state getters/setters and clearing 52 | LocalStorageManager.prototype.getGameState = function () { 53 | var stateJSON = this.storage.getItem(this.gameStateKey); 54 | return stateJSON ? JSON.parse(stateJSON) : null; 55 | }; 56 | 57 | LocalStorageManager.prototype.setGameState = function (gameState) { 58 | this.storage.setItem(this.gameStateKey, JSON.stringify(gameState)); 59 | }; 60 | 61 | LocalStorageManager.prototype.clearGameState = function () { 62 | this.storage.removeItem(this.gameStateKey); 63 | }; 64 | -------------------------------------------------------------------------------- /js/classlist_polyfill.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | if (typeof window.Element === "undefined" || 3 | "classList" in document.documentElement) { 4 | return; 5 | } 6 | 7 | var prototype = Array.prototype, 8 | push = prototype.push, 9 | splice = prototype.splice, 10 | join = prototype.join; 11 | 12 | function DOMTokenList(el) { 13 | this.el = el; 14 | // The className needs to be trimmed and split on whitespace 15 | // to retrieve a list of classes. 16 | var classes = el.className.replace(/^\s+|\s+$/g, '').split(/\s+/); 17 | for (var i = 0; i < classes.length; i++) { 18 | push.call(this, classes[i]); 19 | } 20 | } 21 | 22 | DOMTokenList.prototype = { 23 | add: function (token) { 24 | if (this.contains(token)) return; 25 | push.call(this, token); 26 | this.el.className = this.toString(); 27 | }, 28 | contains: function (token) { 29 | return this.el.className.indexOf(token) != -1; 30 | }, 31 | item: function (index) { 32 | return this[index] || null; 33 | }, 34 | remove: function (token) { 35 | if (!this.contains(token)) return; 36 | for (var i = 0; i < this.length; i++) { 37 | if (this[i] == token) break; 38 | } 39 | splice.call(this, i, 1); 40 | this.el.className = this.toString(); 41 | }, 42 | toString: function () { 43 | return join.call(this, ' '); 44 | }, 45 | toggle: function (token) { 46 | if (!this.contains(token)) { 47 | this.add(token); 48 | } else { 49 | this.remove(token); 50 | } 51 | 52 | return this.contains(token); 53 | } 54 | }; 55 | 56 | window.DOMTokenList = DOMTokenList; 57 | 58 | function defineElementGetter(obj, prop, getter) { 59 | if (Object.defineProperty) { 60 | Object.defineProperty(obj, prop, { 61 | get: getter 62 | }); 63 | } else { 64 | obj.__defineGetter__(prop, getter); 65 | } 66 | } 67 | 68 | defineElementGetter(HTMLElement.prototype, 'classList', function () { 69 | return new DOMTokenList(this); 70 | }); 71 | })(); 72 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 2048 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |
22 |
23 |
0
24 |
0
25 | 2048 - New Game 26 |
27 |
28 |
29 |
30 |
31 | Keep going 32 | Try again 33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | 63 |
64 |
65 |
66 |
67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /js/grid.js: -------------------------------------------------------------------------------- 1 | function Grid(size, previousState) { 2 | this.size = size; 3 | this.cells = previousState ? this.fromState(previousState) : this.empty(); 4 | } 5 | 6 | // Build a grid of the specified size 7 | Grid.prototype.empty = function () { 8 | var cells = []; 9 | 10 | for (var x = 0; x < this.size; x++) { 11 | var row = cells[x] = []; 12 | 13 | for (var y = 0; y < this.size; y++) { 14 | row.push(null); 15 | } 16 | } 17 | 18 | return cells; 19 | }; 20 | 21 | Grid.prototype.fromState = function (state) { 22 | var cells = []; 23 | 24 | for (var x = 0; x < this.size; x++) { 25 | var row = cells[x] = []; 26 | 27 | for (var y = 0; y < this.size; y++) { 28 | var tile = state[x][y]; 29 | row.push(tile ? new Tile(tile.position, tile.value) : null); 30 | } 31 | } 32 | 33 | return cells; 34 | }; 35 | 36 | // Find the first available random position 37 | Grid.prototype.randomAvailableCell = function () { 38 | var cells = this.availableCells(); 39 | 40 | if (cells.length) { 41 | return cells[Math.floor(Math.random() * cells.length)]; 42 | } 43 | }; 44 | 45 | Grid.prototype.availableCells = function () { 46 | var cells = []; 47 | 48 | this.eachCell(function (x, y, tile) { 49 | if (!tile) { 50 | cells.push({ x: x, y: y }); 51 | } 52 | }); 53 | 54 | return cells; 55 | }; 56 | 57 | // Call callback for every cell 58 | Grid.prototype.eachCell = function (callback) { 59 | for (var x = 0; x < this.size; x++) { 60 | for (var y = 0; y < this.size; y++) { 61 | callback(x, y, this.cells[x][y]); 62 | } 63 | } 64 | }; 65 | 66 | // Check if there are any cells available 67 | Grid.prototype.cellsAvailable = function () { 68 | return !!this.availableCells().length; 69 | }; 70 | 71 | // Check if the specified cell is taken 72 | Grid.prototype.cellAvailable = function (cell) { 73 | return !this.cellOccupied(cell); 74 | }; 75 | 76 | Grid.prototype.cellOccupied = function (cell) { 77 | return !!this.cellContent(cell); 78 | }; 79 | 80 | Grid.prototype.cellContent = function (cell) { 81 | if (this.withinBounds(cell)) { 82 | return this.cells[cell.x][cell.y]; 83 | } else { 84 | return null; 85 | } 86 | }; 87 | 88 | // Inserts a tile at its position 89 | Grid.prototype.insertTile = function (tile) { 90 | this.cells[tile.x][tile.y] = tile; 91 | }; 92 | 93 | Grid.prototype.removeTile = function (tile) { 94 | this.cells[tile.x][tile.y] = null; 95 | }; 96 | 97 | Grid.prototype.withinBounds = function (position) { 98 | return position.x >= 0 && position.x < this.size && 99 | position.y >= 0 && position.y < this.size; 100 | }; 101 | 102 | Grid.prototype.serialize = function () { 103 | var cellState = []; 104 | 105 | for (var x = 0; x < this.size; x++) { 106 | var row = cellState[x] = []; 107 | 108 | for (var y = 0; y < this.size; y++) { 109 | row.push(this.cells[x][y] ? this.cells[x][y].serialize() : null); 110 | } 111 | } 112 | 113 | return { 114 | size: this.size, 115 | cells: cellState 116 | }; 117 | }; 118 | -------------------------------------------------------------------------------- /js/html_actuator.js: -------------------------------------------------------------------------------- 1 | function HTMLActuator() { 2 | this.tileContainer = document.querySelector(".tile-container"); 3 | this.scoreContainer = document.querySelector(".score-container"); 4 | this.bestContainer = document.querySelector(".best-container"); 5 | this.messageContainer = document.querySelector(".game-message"); 6 | 7 | this.score = 0; 8 | } 9 | 10 | HTMLActuator.prototype.actuate = function (grid, metadata) { 11 | var self = this; 12 | 13 | window.requestAnimationFrame(function () { 14 | self.clearContainer(self.tileContainer); 15 | 16 | grid.cells.forEach(function (column) { 17 | column.forEach(function (cell) { 18 | if (cell) { 19 | self.addTile(cell); 20 | } 21 | }); 22 | }); 23 | 24 | self.updateScore(metadata.score); 25 | self.updateBestScore(metadata.bestScore); 26 | 27 | if (metadata.terminated) { 28 | if (metadata.over) { 29 | self.message(false); // You lose 30 | } else if (metadata.won) { 31 | self.message(true); // You win! 32 | } 33 | } 34 | 35 | }); 36 | }; 37 | 38 | // Continues the game (both restart and keep playing) 39 | HTMLActuator.prototype.continueGame = function () { 40 | this.clearMessage(); 41 | }; 42 | 43 | HTMLActuator.prototype.clearContainer = function (container) { 44 | while (container.firstChild) { 45 | container.removeChild(container.firstChild); 46 | } 47 | }; 48 | 49 | HTMLActuator.prototype.addTile = function (tile) { 50 | var self = this; 51 | 52 | var wrapper = document.createElement("div"); 53 | var inner = document.createElement("div"); 54 | var position = tile.previousPosition || { x: tile.x, y: tile.y }; 55 | var positionClass = this.positionClass(position); 56 | 57 | // We can't use classlist because it somehow glitches when replacing classes 58 | var classes = ["tile", "tile-" + tile.value, positionClass]; 59 | 60 | if (tile.value > 2048) classes.push("tile-super"); 61 | 62 | this.applyClasses(wrapper, classes); 63 | 64 | inner.classList.add("tile-inner"); 65 | inner.textContent = tile.value; 66 | 67 | if (tile.previousPosition) { 68 | // Make sure that the tile gets rendered in the previous position first 69 | window.requestAnimationFrame(function () { 70 | classes[2] = self.positionClass({ x: tile.x, y: tile.y }); 71 | self.applyClasses(wrapper, classes); // Update the position 72 | }); 73 | } else if (tile.mergedFrom) { 74 | classes.push("tile-merged"); 75 | this.applyClasses(wrapper, classes); 76 | 77 | // Render the tiles that merged 78 | tile.mergedFrom.forEach(function (merged) { 79 | self.addTile(merged); 80 | }); 81 | } else { 82 | classes.push("tile-new"); 83 | this.applyClasses(wrapper, classes); 84 | } 85 | 86 | // Add the inner part of the tile to the wrapper 87 | wrapper.appendChild(inner); 88 | 89 | // Put the tile on the board 90 | this.tileContainer.appendChild(wrapper); 91 | }; 92 | 93 | HTMLActuator.prototype.applyClasses = function (element, classes) { 94 | element.setAttribute("class", classes.join(" ")); 95 | }; 96 | 97 | HTMLActuator.prototype.normalizePosition = function (position) { 98 | return { x: position.x + 1, y: position.y + 1 }; 99 | }; 100 | 101 | HTMLActuator.prototype.positionClass = function (position) { 102 | position = this.normalizePosition(position); 103 | return "tile-position-" + position.x + "-" + position.y; 104 | }; 105 | 106 | HTMLActuator.prototype.updateScore = function (score) { 107 | this.clearContainer(this.scoreContainer); 108 | 109 | var difference = score - this.score; 110 | this.score = score; 111 | 112 | this.scoreContainer.textContent = this.score; 113 | 114 | if (difference > 0) { 115 | var addition = document.createElement("div"); 116 | addition.classList.add("score-addition"); 117 | addition.textContent = "+" + difference; 118 | 119 | this.scoreContainer.appendChild(addition); 120 | } 121 | }; 122 | 123 | HTMLActuator.prototype.updateBestScore = function (bestScore) { 124 | this.bestContainer.textContent = bestScore; 125 | }; 126 | 127 | HTMLActuator.prototype.message = function (won) { 128 | var type = won ? "game-won" : "game-over"; 129 | var message = won ? "You win!" : "Game over!"; 130 | 131 | this.messageContainer.classList.add(type); 132 | this.messageContainer.getElementsByTagName("p")[0].textContent = message; 133 | }; 134 | 135 | HTMLActuator.prototype.clearMessage = function () { 136 | // IE only takes one value to remove at a time. 137 | this.messageContainer.classList.remove("game-won"); 138 | this.messageContainer.classList.remove("game-over"); 139 | }; 140 | -------------------------------------------------------------------------------- /js/keyboard_input_manager.js: -------------------------------------------------------------------------------- 1 | function KeyboardInputManager() { 2 | this.events = {}; 3 | 4 | if (window.navigator.msPointerEnabled) { 5 | //Internet Explorer 10 style 6 | this.eventTouchstart = "MSPointerDown"; 7 | this.eventTouchmove = "MSPointerMove"; 8 | this.eventTouchend = "MSPointerUp"; 9 | } else { 10 | this.eventTouchstart = "touchstart"; 11 | this.eventTouchmove = "touchmove"; 12 | this.eventTouchend = "touchend"; 13 | } 14 | 15 | this.listen(); 16 | this.setupGamepad(); 17 | } 18 | 19 | KeyboardInputManager.prototype.setupGamepad = function () { 20 | var self = this; 21 | this.lastGamepadState = { up: false, down: false, left: false, right: false }; 22 | 23 | // Start polling for gamepad input 24 | this.pollGamepad(); 25 | }; 26 | 27 | KeyboardInputManager.prototype.pollGamepad = function () { 28 | var self = this; 29 | 30 | // Check for connected gamepads 31 | var gamepads = navigator.getGamepads(); 32 | if (gamepads && gamepads[0]) { 33 | var gamepad = gamepads[0]; 34 | 35 | // Check d-pad buttons (buttons 12-15 are standard d-pad) 36 | var currentState = { 37 | up: gamepad.buttons[12] && gamepad.buttons[12].pressed, 38 | down: gamepad.buttons[13] && gamepad.buttons[13].pressed, 39 | left: gamepad.buttons[14] && gamepad.buttons[14].pressed, 40 | right: gamepad.buttons[15] && gamepad.buttons[15].pressed 41 | }; 42 | 43 | // Emit move events for newly pressed buttons (not held) 44 | if (currentState.up && !this.lastGamepadState.up) { 45 | self.emit("move", 0); // Up 46 | } 47 | if (currentState.right && !this.lastGamepadState.right) { 48 | self.emit("move", 1); // Right 49 | } 50 | if (currentState.down && !this.lastGamepadState.down) { 51 | self.emit("move", 2); // Down 52 | } 53 | if (currentState.left && !this.lastGamepadState.left) { 54 | self.emit("move", 3); // Left 55 | } 56 | 57 | this.lastGamepadState = currentState; 58 | } 59 | 60 | // Continue polling 61 | requestAnimationFrame(function() { 62 | self.pollGamepad(); 63 | }); 64 | }; 65 | 66 | KeyboardInputManager.prototype.on = function (event, callback) { 67 | if (!this.events[event]) { 68 | this.events[event] = []; 69 | } 70 | this.events[event].push(callback); 71 | }; 72 | 73 | KeyboardInputManager.prototype.emit = function (event, data) { 74 | var callbacks = this.events[event]; 75 | if (callbacks) { 76 | callbacks.forEach(function (callback) { 77 | callback(data); 78 | }); 79 | } 80 | }; 81 | 82 | KeyboardInputManager.prototype.listen = function () { 83 | var self = this; 84 | 85 | var map = { 86 | 38: 0, // Up 87 | 39: 1, // Right 88 | 40: 2, // Down 89 | 37: 3, // Left 90 | 75: 0, // Vim up 91 | 76: 1, // Vim right 92 | 74: 2, // Vim down 93 | 72: 3, // Vim left 94 | 87: 0, // W 95 | 68: 1, // D 96 | 83: 2, // S 97 | 65: 3 // A 98 | }; 99 | 100 | // Respond to direction keys 101 | document.addEventListener("keydown", function (event) { 102 | var modifiers = event.altKey || event.ctrlKey || event.metaKey || 103 | event.shiftKey; 104 | var mapped = map[event.which]; 105 | 106 | if (!modifiers) { 107 | if (mapped !== undefined) { 108 | event.preventDefault(); 109 | self.emit("move", mapped); 110 | } 111 | } 112 | 113 | // R key restarts the game 114 | if (!modifiers && event.which === 82) { 115 | self.restart.call(self, event); 116 | } 117 | }); 118 | 119 | // Respond to button presses 120 | this.bindButtonPress(".retry-button", this.restart); 121 | this.bindButtonPress(".restart-button", this.restart); 122 | this.bindButtonPress(".keep-playing-button", this.keepPlaying); 123 | 124 | // Respond to swipe events 125 | var touchStartClientX, touchStartClientY; 126 | var gameContainer = document.getElementsByClassName("game-container")[0]; 127 | 128 | gameContainer.addEventListener(this.eventTouchstart, function (event) { 129 | if ((!window.navigator.msPointerEnabled && event.touches.length > 1) || 130 | event.targetTouches.length > 1) { 131 | return; // Ignore if touching with more than 1 finger 132 | } 133 | 134 | if (window.navigator.msPointerEnabled) { 135 | touchStartClientX = event.pageX; 136 | touchStartClientY = event.pageY; 137 | } else { 138 | touchStartClientX = event.touches[0].clientX; 139 | touchStartClientY = event.touches[0].clientY; 140 | } 141 | 142 | event.preventDefault(); 143 | }); 144 | 145 | gameContainer.addEventListener(this.eventTouchmove, function (event) { 146 | event.preventDefault(); 147 | }); 148 | 149 | gameContainer.addEventListener(this.eventTouchend, function (event) { 150 | if ((!window.navigator.msPointerEnabled && event.touches.length > 0) || 151 | event.targetTouches.length > 0) { 152 | return; // Ignore if still touching with one or more fingers 153 | } 154 | 155 | var touchEndClientX, touchEndClientY; 156 | 157 | if (window.navigator.msPointerEnabled) { 158 | touchEndClientX = event.pageX; 159 | touchEndClientY = event.pageY; 160 | } else { 161 | touchEndClientX = event.changedTouches[0].clientX; 162 | touchEndClientY = event.changedTouches[0].clientY; 163 | } 164 | 165 | var dx = touchEndClientX - touchStartClientX; 166 | var absDx = Math.abs(dx); 167 | 168 | var dy = touchEndClientY - touchStartClientY; 169 | var absDy = Math.abs(dy); 170 | 171 | if (Math.max(absDx, absDy) > 10) { 172 | // (right : left) : (down : up) 173 | self.emit("move", absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0)); 174 | } 175 | }); 176 | }; 177 | 178 | KeyboardInputManager.prototype.restart = function (event) { 179 | event.preventDefault(); 180 | this.emit("restart"); 181 | }; 182 | 183 | KeyboardInputManager.prototype.keepPlaying = function (event) { 184 | event.preventDefault(); 185 | this.emit("keepPlaying"); 186 | }; 187 | 188 | KeyboardInputManager.prototype.bindButtonPress = function (selector, fn) { 189 | var button = document.querySelector(selector); 190 | button.addEventListener("click", fn.bind(this)); 191 | button.addEventListener(this.eventTouchend, fn.bind(this)); 192 | }; 193 | -------------------------------------------------------------------------------- /js/game_manager.js: -------------------------------------------------------------------------------- 1 | function GameManager(size, InputManager, Actuator, StorageManager) { 2 | this.size = size; // Size of the grid 3 | this.inputManager = new InputManager; 4 | this.storageManager = new StorageManager; 5 | this.actuator = new Actuator; 6 | 7 | this.startTiles = 2; 8 | 9 | this.inputManager.on("move", this.move.bind(this)); 10 | this.inputManager.on("restart", this.restart.bind(this)); 11 | this.inputManager.on("keepPlaying", this.keepPlaying.bind(this)); 12 | 13 | this.setup(); 14 | } 15 | 16 | // Restart the game 17 | GameManager.prototype.restart = function () { 18 | this.storageManager.clearGameState(); 19 | this.actuator.continueGame(); // Clear the game won/lost message 20 | this.setup(); 21 | }; 22 | 23 | // Keep playing after winning (allows going over 2048) 24 | GameManager.prototype.keepPlaying = function () { 25 | this.keepPlaying = true; 26 | this.actuator.continueGame(); // Clear the game won/lost message 27 | }; 28 | 29 | // Return true if the game is lost, or has won and the user hasn't kept playing 30 | GameManager.prototype.isGameTerminated = function () { 31 | return this.over || (this.won && !this.keepPlaying); 32 | }; 33 | 34 | // Set up the game 35 | GameManager.prototype.setup = function () { 36 | var previousState = this.storageManager.getGameState(); 37 | 38 | // Reload the game from a previous game if present 39 | if (previousState) { 40 | this.grid = new Grid(previousState.grid.size, 41 | previousState.grid.cells); // Reload grid 42 | this.score = previousState.score; 43 | this.over = previousState.over; 44 | this.won = previousState.won; 45 | this.keepPlaying = previousState.keepPlaying; 46 | } else { 47 | this.grid = new Grid(this.size); 48 | this.score = 0; 49 | this.over = false; 50 | this.won = false; 51 | this.keepPlaying = false; 52 | 53 | // Add the initial tiles 54 | this.addStartTiles(); 55 | } 56 | 57 | // Update the actuator 58 | this.actuate(); 59 | }; 60 | 61 | // Set up the initial tiles to start the game with 62 | GameManager.prototype.addStartTiles = function () { 63 | for (var i = 0; i < this.startTiles; i++) { 64 | this.addRandomTile(); 65 | } 66 | }; 67 | 68 | // Adds a tile in a random position 69 | GameManager.prototype.addRandomTile = function () { 70 | if (this.grid.cellsAvailable()) { 71 | var value = Math.random() < 0.9 ? 2 : 4; 72 | var tile = new Tile(this.grid.randomAvailableCell(), value); 73 | 74 | this.grid.insertTile(tile); 75 | } 76 | }; 77 | 78 | // Sends the updated grid to the actuator 79 | GameManager.prototype.actuate = function () { 80 | if (this.storageManager.getBestScore() < this.score) { 81 | this.storageManager.setBestScore(this.score); 82 | } 83 | 84 | // Clear the state when the game is over (game over only, not win) 85 | if (this.over) { 86 | this.storageManager.clearGameState(); 87 | } else { 88 | this.storageManager.setGameState(this.serialize()); 89 | } 90 | 91 | this.actuator.actuate(this.grid, { 92 | score: this.score, 93 | over: this.over, 94 | won: this.won, 95 | bestScore: this.storageManager.getBestScore(), 96 | terminated: this.isGameTerminated() 97 | }); 98 | 99 | }; 100 | 101 | // Represent the current game as an object 102 | GameManager.prototype.serialize = function () { 103 | return { 104 | grid: this.grid.serialize(), 105 | score: this.score, 106 | over: this.over, 107 | won: this.won, 108 | keepPlaying: this.keepPlaying 109 | }; 110 | }; 111 | 112 | // Save all tile positions and remove merger info 113 | GameManager.prototype.prepareTiles = function () { 114 | this.grid.eachCell(function (x, y, tile) { 115 | if (tile) { 116 | tile.mergedFrom = null; 117 | tile.savePosition(); 118 | } 119 | }); 120 | }; 121 | 122 | // Move a tile and its representation 123 | GameManager.prototype.moveTile = function (tile, cell) { 124 | this.grid.cells[tile.x][tile.y] = null; 125 | this.grid.cells[cell.x][cell.y] = tile; 126 | tile.updatePosition(cell); 127 | }; 128 | 129 | // Move tiles on the grid in the specified direction 130 | GameManager.prototype.move = function (direction) { 131 | // 0: up, 1: right, 2: down, 3: left 132 | var self = this; 133 | 134 | if (this.isGameTerminated()) return; // Don't do anything if the game's over 135 | 136 | var cell, tile; 137 | 138 | var vector = this.getVector(direction); 139 | var traversals = this.buildTraversals(vector); 140 | var moved = false; 141 | 142 | // Save the current tile positions and remove merger information 143 | this.prepareTiles(); 144 | 145 | // Traverse the grid in the right direction and move tiles 146 | traversals.x.forEach(function (x) { 147 | traversals.y.forEach(function (y) { 148 | cell = { x: x, y: y }; 149 | tile = self.grid.cellContent(cell); 150 | 151 | if (tile) { 152 | var positions = self.findFarthestPosition(cell, vector); 153 | var next = self.grid.cellContent(positions.next); 154 | 155 | // Only one merger per row traversal? 156 | if (next && next.value === tile.value && !next.mergedFrom) { 157 | var merged = new Tile(positions.next, tile.value * 2); 158 | merged.mergedFrom = [tile, next]; 159 | 160 | self.grid.insertTile(merged); 161 | self.grid.removeTile(tile); 162 | 163 | // Converge the two tiles' positions 164 | tile.updatePosition(positions.next); 165 | 166 | // Update the score 167 | self.score += merged.value; 168 | 169 | // The mighty 2048 tile 170 | if (merged.value === 2048) self.won = true; 171 | } else { 172 | self.moveTile(tile, positions.farthest); 173 | } 174 | 175 | if (!self.positionsEqual(cell, tile)) { 176 | moved = true; // The tile moved from its original cell! 177 | } 178 | } 179 | }); 180 | }); 181 | 182 | if (moved) { 183 | this.addRandomTile(); 184 | 185 | if (!this.movesAvailable()) { 186 | this.over = true; // Game over! 187 | } 188 | 189 | this.actuate(); 190 | } 191 | }; 192 | 193 | // Get the vector representing the chosen direction 194 | GameManager.prototype.getVector = function (direction) { 195 | // Vectors representing tile movement 196 | var map = { 197 | 0: { x: 0, y: -1 }, // Up 198 | 1: { x: 1, y: 0 }, // Right 199 | 2: { x: 0, y: 1 }, // Down 200 | 3: { x: -1, y: 0 } // Left 201 | }; 202 | 203 | return map[direction]; 204 | }; 205 | 206 | // Build a list of positions to traverse in the right order 207 | GameManager.prototype.buildTraversals = function (vector) { 208 | var traversals = { x: [], y: [] }; 209 | 210 | for (var pos = 0; pos < this.size; pos++) { 211 | traversals.x.push(pos); 212 | traversals.y.push(pos); 213 | } 214 | 215 | // Always traverse from the farthest cell in the chosen direction 216 | if (vector.x === 1) traversals.x = traversals.x.reverse(); 217 | if (vector.y === 1) traversals.y = traversals.y.reverse(); 218 | 219 | return traversals; 220 | }; 221 | 222 | GameManager.prototype.findFarthestPosition = function (cell, vector) { 223 | var previous; 224 | 225 | // Progress towards the vector direction until an obstacle is found 226 | do { 227 | previous = cell; 228 | cell = { x: previous.x + vector.x, y: previous.y + vector.y }; 229 | } while (this.grid.withinBounds(cell) && 230 | this.grid.cellAvailable(cell)); 231 | 232 | return { 233 | farthest: previous, 234 | next: cell // Used to check if a merge is required 235 | }; 236 | }; 237 | 238 | GameManager.prototype.movesAvailable = function () { 239 | return this.grid.cellsAvailable() || this.tileMatchesAvailable(); 240 | }; 241 | 242 | // Check for available matches between tiles (more expensive check) 243 | GameManager.prototype.tileMatchesAvailable = function () { 244 | var self = this; 245 | 246 | var tile; 247 | 248 | for (var x = 0; x < this.size; x++) { 249 | for (var y = 0; y < this.size; y++) { 250 | tile = this.grid.cellContent({ x: x, y: y }); 251 | 252 | if (tile) { 253 | for (var direction = 0; direction < 4; direction++) { 254 | var vector = self.getVector(direction); 255 | var cell = { x: x + vector.x, y: y + vector.y }; 256 | 257 | var other = self.grid.cellContent(cell); 258 | 259 | if (other && other.value === tile.value) { 260 | return true; // These two tiles can be merged 261 | } 262 | } 263 | } 264 | } 265 | } 266 | 267 | return false; 268 | }; 269 | 270 | GameManager.prototype.positionsEqual = function (first, second) { 271 | return first.x === second.x && first.y === second.y; 272 | }; 273 | -------------------------------------------------------------------------------- /style/main.css: -------------------------------------------------------------------------------- 1 | @import url(fonts/clear-sans.css); 2 | html, body { 3 | margin: 0; 4 | padding: 0; 5 | background: #faf8ef; 6 | color: #776e65; 7 | font-family: "Clear Sans", "Helvetica Neue", Arial, sans-serif; 8 | font-size: 18px; } 9 | 10 | body { 11 | margin: 10px 0; } 12 | 13 | .heading:after { 14 | content: ""; 15 | display: block; 16 | clear: both; } 17 | 18 | h1.title { 19 | font-size: 80px; 20 | font-weight: bold; 21 | margin: 0; 22 | display: block; 23 | float: left; } 24 | 25 | @-webkit-keyframes move-up { 26 | 0% { 27 | top: 25px; 28 | opacity: 1; } 29 | 30 | 100% { 31 | top: -50px; 32 | opacity: 0; } } 33 | @-moz-keyframes move-up { 34 | 0% { 35 | top: 25px; 36 | opacity: 1; } 37 | 38 | 100% { 39 | top: -50px; 40 | opacity: 0; } } 41 | @keyframes move-up { 42 | 0% { 43 | top: 25px; 44 | opacity: 1; } 45 | 46 | 100% { 47 | top: -50px; 48 | opacity: 0; } } 49 | .scores-container { 50 | float: left; 51 | text-align: right; } 52 | 53 | .score-container, .best-container { 54 | position: relative; 55 | display: inline-block; 56 | background: #bbada0; 57 | padding: 15px 25px; 58 | font-size: 25px; 59 | height: 25px; 60 | line-height: 47px; 61 | font-weight: bold; 62 | border-radius: 3px; 63 | color: white; 64 | margin-top: 0px; 65 | text-align: center; } 66 | .score-container:after, .best-container:after { 67 | position: absolute; 68 | width: 100%; 69 | top: 10px; 70 | left: 0; 71 | text-transform: uppercase; 72 | font-size: 13px; 73 | line-height: 13px; 74 | text-align: center; 75 | color: #eee4da; } 76 | .score-container .score-addition, .best-container .score-addition { 77 | position: absolute; 78 | right: 30px; 79 | color: red; 80 | font-size: 25px; 81 | line-height: 25px; 82 | font-weight: bold; 83 | color: rgba(119, 110, 101, 0.9); 84 | z-index: 100; 85 | -webkit-animation: move-up 600ms ease-in; 86 | -moz-animation: move-up 600ms ease-in; 87 | animation: move-up 600ms ease-in; 88 | -webkit-animation-fill-mode: both; 89 | -moz-animation-fill-mode: both; 90 | animation-fill-mode: both; } 91 | 92 | .score-container:after { 93 | content: "Score"; } 94 | 95 | .best-container:after { 96 | content: "Best"; } 97 | 98 | p { 99 | margin-top: 0; 100 | margin-bottom: 10px; 101 | line-height: 1.65; } 102 | 103 | a { 104 | color: #776e65; 105 | font-weight: bold; 106 | text-decoration: underline; 107 | cursor: pointer; } 108 | 109 | strong.important { 110 | text-transform: uppercase; } 111 | 112 | hr { 113 | border: none; 114 | border-bottom: 1px solid #d8d4d0; 115 | margin-top: 20px; 116 | margin-bottom: 30px; } 117 | 118 | .container { 119 | width: 500px; 120 | margin: 0 auto; } 121 | 122 | @-webkit-keyframes fade-in { 123 | 0% { 124 | opacity: 0; } 125 | 126 | 100% { 127 | opacity: 1; } } 128 | @-moz-keyframes fade-in { 129 | 0% { 130 | opacity: 0; } 131 | 132 | 100% { 133 | opacity: 1; } } 134 | @keyframes fade-in { 135 | 0% { 136 | opacity: 0; } 137 | 138 | 100% { 139 | opacity: 1; } } 140 | .game-container { 141 | margin-top: 40px; 142 | position: relative; 143 | padding: 15px; 144 | cursor: default; 145 | -webkit-touch-callout: none; 146 | -ms-touch-callout: none; 147 | -webkit-user-select: none; 148 | -moz-user-select: none; 149 | -ms-user-select: none; 150 | -ms-touch-action: none; 151 | touch-action: none; 152 | background: #bbada0; 153 | border-radius: 6px; 154 | width: 500px; 155 | height: 500px; 156 | -webkit-box-sizing: border-box; 157 | -moz-box-sizing: border-box; 158 | box-sizing: border-box; } 159 | .game-container .game-message { 160 | display: none; 161 | position: absolute; 162 | top: 0; 163 | right: 0; 164 | bottom: 0; 165 | left: 0; 166 | background: rgba(238, 228, 218, 0.5); 167 | z-index: 100; 168 | text-align: center; 169 | -webkit-animation: fade-in 800ms ease 1200ms; 170 | -moz-animation: fade-in 800ms ease 1200ms; 171 | animation: fade-in 800ms ease 1200ms; 172 | -webkit-animation-fill-mode: both; 173 | -moz-animation-fill-mode: both; 174 | animation-fill-mode: both; } 175 | .game-container .game-message p { 176 | font-size: 60px; 177 | font-weight: bold; 178 | height: 60px; 179 | line-height: 60px; 180 | margin-top: 222px; } 181 | .game-container .game-message .lower { 182 | display: block; 183 | margin-top: 59px; } 184 | .game-container .game-message a { 185 | display: inline-block; 186 | background: #8f7a66; 187 | border-radius: 3px; 188 | padding: 0 20px; 189 | text-decoration: none; 190 | color: #f9f6f2; 191 | height: 40px; 192 | line-height: 42px; 193 | margin-left: 9px; } 194 | .game-container .game-message a.keep-playing-button { 195 | display: none; } 196 | .game-container .game-message.game-won { 197 | background: rgba(237, 194, 46, 0.5); 198 | color: #f9f6f2; } 199 | .game-container .game-message.game-won a.keep-playing-button { 200 | display: inline-block; } 201 | .game-container .game-message.game-won, .game-container .game-message.game-over { 202 | display: block; } 203 | 204 | .grid-container { 205 | position: absolute; 206 | z-index: 1; } 207 | 208 | .grid-row { 209 | margin-bottom: 15px; } 210 | .grid-row:last-child { 211 | margin-bottom: 0; } 212 | .grid-row:after { 213 | content: ""; 214 | display: block; 215 | clear: both; } 216 | 217 | .grid-cell { 218 | width: 106.25px; 219 | height: 106.25px; 220 | margin-right: 15px; 221 | float: left; 222 | border-radius: 3px; 223 | background: rgba(238, 228, 218, 0.35); } 224 | .grid-cell:last-child { 225 | margin-right: 0; } 226 | 227 | .tile-container { 228 | position: absolute; 229 | z-index: 2; } 230 | 231 | .tile, .tile .tile-inner { 232 | width: 107px; 233 | height: 107px; 234 | line-height: 107px; } 235 | .tile.tile-position-1-1 { 236 | -webkit-transform: translate(0px, 0px); 237 | -moz-transform: translate(0px, 0px); 238 | -ms-transform: translate(0px, 0px); 239 | transform: translate(0px, 0px); } 240 | .tile.tile-position-1-2 { 241 | -webkit-transform: translate(0px, 121px); 242 | -moz-transform: translate(0px, 121px); 243 | -ms-transform: translate(0px, 121px); 244 | transform: translate(0px, 121px); } 245 | .tile.tile-position-1-3 { 246 | -webkit-transform: translate(0px, 242px); 247 | -moz-transform: translate(0px, 242px); 248 | -ms-transform: translate(0px, 242px); 249 | transform: translate(0px, 242px); } 250 | .tile.tile-position-1-4 { 251 | -webkit-transform: translate(0px, 363px); 252 | -moz-transform: translate(0px, 363px); 253 | -ms-transform: translate(0px, 363px); 254 | transform: translate(0px, 363px); } 255 | .tile.tile-position-2-1 { 256 | -webkit-transform: translate(121px, 0px); 257 | -moz-transform: translate(121px, 0px); 258 | -ms-transform: translate(121px, 0px); 259 | transform: translate(121px, 0px); } 260 | .tile.tile-position-2-2 { 261 | -webkit-transform: translate(121px, 121px); 262 | -moz-transform: translate(121px, 121px); 263 | -ms-transform: translate(121px, 121px); 264 | transform: translate(121px, 121px); } 265 | .tile.tile-position-2-3 { 266 | -webkit-transform: translate(121px, 242px); 267 | -moz-transform: translate(121px, 242px); 268 | -ms-transform: translate(121px, 242px); 269 | transform: translate(121px, 242px); } 270 | .tile.tile-position-2-4 { 271 | -webkit-transform: translate(121px, 363px); 272 | -moz-transform: translate(121px, 363px); 273 | -ms-transform: translate(121px, 363px); 274 | transform: translate(121px, 363px); } 275 | .tile.tile-position-3-1 { 276 | -webkit-transform: translate(242px, 0px); 277 | -moz-transform: translate(242px, 0px); 278 | -ms-transform: translate(242px, 0px); 279 | transform: translate(242px, 0px); } 280 | .tile.tile-position-3-2 { 281 | -webkit-transform: translate(242px, 121px); 282 | -moz-transform: translate(242px, 121px); 283 | -ms-transform: translate(242px, 121px); 284 | transform: translate(242px, 121px); } 285 | .tile.tile-position-3-3 { 286 | -webkit-transform: translate(242px, 242px); 287 | -moz-transform: translate(242px, 242px); 288 | -ms-transform: translate(242px, 242px); 289 | transform: translate(242px, 242px); } 290 | .tile.tile-position-3-4 { 291 | -webkit-transform: translate(242px, 363px); 292 | -moz-transform: translate(242px, 363px); 293 | -ms-transform: translate(242px, 363px); 294 | transform: translate(242px, 363px); } 295 | .tile.tile-position-4-1 { 296 | -webkit-transform: translate(363px, 0px); 297 | -moz-transform: translate(363px, 0px); 298 | -ms-transform: translate(363px, 0px); 299 | transform: translate(363px, 0px); } 300 | .tile.tile-position-4-2 { 301 | -webkit-transform: translate(363px, 121px); 302 | -moz-transform: translate(363px, 121px); 303 | -ms-transform: translate(363px, 121px); 304 | transform: translate(363px, 121px); } 305 | .tile.tile-position-4-3 { 306 | -webkit-transform: translate(363px, 242px); 307 | -moz-transform: translate(363px, 242px); 308 | -ms-transform: translate(363px, 242px); 309 | transform: translate(363px, 242px); } 310 | .tile.tile-position-4-4 { 311 | -webkit-transform: translate(363px, 363px); 312 | -moz-transform: translate(363px, 363px); 313 | -ms-transform: translate(363px, 363px); 314 | transform: translate(363px, 363px); } 315 | 316 | .tile { 317 | position: absolute; 318 | -webkit-transition: 100ms ease-in-out; 319 | -moz-transition: 100ms ease-in-out; 320 | transition: 100ms ease-in-out; 321 | -webkit-transition-property: -webkit-transform; 322 | -moz-transition-property: -moz-transform; 323 | transition-property: transform; } 324 | .tile .tile-inner { 325 | border-radius: 3px; 326 | background: #eee4da; 327 | text-align: center; 328 | font-weight: bold; 329 | z-index: 10; 330 | font-size: 55px; } 331 | .tile.tile-2 .tile-inner { 332 | background: #eee4da; 333 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0); } 334 | .tile.tile-4 .tile-inner { 335 | background: #ede0c8; 336 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0); } 337 | .tile.tile-8 .tile-inner { 338 | color: #f9f6f2; 339 | background: #f2b179; } 340 | .tile.tile-16 .tile-inner { 341 | color: #f9f6f2; 342 | background: #f59563; } 343 | .tile.tile-32 .tile-inner { 344 | color: #f9f6f2; 345 | background: #f67c5f; } 346 | .tile.tile-64 .tile-inner { 347 | color: #f9f6f2; 348 | background: #f65e3b; } 349 | .tile.tile-128 .tile-inner { 350 | color: #f9f6f2; 351 | background: #edcf72; 352 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.2381), inset 0 0 0 1px rgba(255, 255, 255, 0.14286); 353 | font-size: 45px; } 354 | @media screen and (max-width: 520px) { 355 | .tile.tile-128 .tile-inner { 356 | font-size: 25px; } } 357 | .tile.tile-256 .tile-inner { 358 | color: #f9f6f2; 359 | background: #edcc61; 360 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.31746), inset 0 0 0 1px rgba(255, 255, 255, 0.19048); 361 | font-size: 45px; } 362 | @media screen and (max-width: 520px) { 363 | .tile.tile-256 .tile-inner { 364 | font-size: 25px; } } 365 | .tile.tile-512 .tile-inner { 366 | color: #f9f6f2; 367 | background: #edc850; 368 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.39683), inset 0 0 0 1px rgba(255, 255, 255, 0.2381); 369 | font-size: 45px; } 370 | @media screen and (max-width: 520px) { 371 | .tile.tile-512 .tile-inner { 372 | font-size: 25px; } } 373 | .tile.tile-1024 .tile-inner { 374 | color: #f9f6f2; 375 | background: #edc53f; 376 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.47619), inset 0 0 0 1px rgba(255, 255, 255, 0.28571); 377 | font-size: 35px; } 378 | @media screen and (max-width: 520px) { 379 | .tile.tile-1024 .tile-inner { 380 | font-size: 15px; } } 381 | .tile.tile-2048 .tile-inner { 382 | color: #f9f6f2; 383 | background: #edc22e; 384 | box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.55556), inset 0 0 0 1px rgba(255, 255, 255, 0.33333); 385 | font-size: 35px; } 386 | @media screen and (max-width: 520px) { 387 | .tile.tile-2048 .tile-inner { 388 | font-size: 15px; } } 389 | .tile.tile-super .tile-inner { 390 | color: #f9f6f2; 391 | background: #3c3a32; 392 | font-size: 30px; } 393 | @media screen and (max-width: 520px) { 394 | .tile.tile-super .tile-inner { 395 | font-size: 10px; } } 396 | 397 | @-webkit-keyframes appear { 398 | 0% { 399 | opacity: 0; 400 | -webkit-transform: scale(0); 401 | -moz-transform: scale(0); 402 | -ms-transform: scale(0); 403 | transform: scale(0); } 404 | 405 | 100% { 406 | opacity: 1; 407 | -webkit-transform: scale(1); 408 | -moz-transform: scale(1); 409 | -ms-transform: scale(1); 410 | transform: scale(1); } } 411 | @-moz-keyframes appear { 412 | 0% { 413 | opacity: 0; 414 | -webkit-transform: scale(0); 415 | -moz-transform: scale(0); 416 | -ms-transform: scale(0); 417 | transform: scale(0); } 418 | 419 | 100% { 420 | opacity: 1; 421 | -webkit-transform: scale(1); 422 | -moz-transform: scale(1); 423 | -ms-transform: scale(1); 424 | transform: scale(1); } } 425 | @keyframes appear { 426 | 0% { 427 | opacity: 0; 428 | -webkit-transform: scale(0); 429 | -moz-transform: scale(0); 430 | -ms-transform: scale(0); 431 | transform: scale(0); } 432 | 433 | 100% { 434 | opacity: 1; 435 | -webkit-transform: scale(1); 436 | -moz-transform: scale(1); 437 | -ms-transform: scale(1); 438 | transform: scale(1); } } 439 | .tile-new .tile-inner { 440 | -webkit-animation: appear 200ms ease 100ms; 441 | -moz-animation: appear 200ms ease 100ms; 442 | animation: appear 200ms ease 100ms; 443 | -webkit-animation-fill-mode: backwards; 444 | -moz-animation-fill-mode: backwards; 445 | animation-fill-mode: backwards; } 446 | 447 | @-webkit-keyframes pop { 448 | 0% { 449 | -webkit-transform: scale(0); 450 | -moz-transform: scale(0); 451 | -ms-transform: scale(0); 452 | transform: scale(0); } 453 | 454 | 50% { 455 | -webkit-transform: scale(1.2); 456 | -moz-transform: scale(1.2); 457 | -ms-transform: scale(1.2); 458 | transform: scale(1.2); } 459 | 460 | 100% { 461 | -webkit-transform: scale(1); 462 | -moz-transform: scale(1); 463 | -ms-transform: scale(1); 464 | transform: scale(1); } } 465 | @-moz-keyframes pop { 466 | 0% { 467 | -webkit-transform: scale(0); 468 | -moz-transform: scale(0); 469 | -ms-transform: scale(0); 470 | transform: scale(0); } 471 | 472 | 50% { 473 | -webkit-transform: scale(1.2); 474 | -moz-transform: scale(1.2); 475 | -ms-transform: scale(1.2); 476 | transform: scale(1.2); } 477 | 478 | 100% { 479 | -webkit-transform: scale(1); 480 | -moz-transform: scale(1); 481 | -ms-transform: scale(1); 482 | transform: scale(1); } } 483 | @keyframes pop { 484 | 0% { 485 | -webkit-transform: scale(0); 486 | -moz-transform: scale(0); 487 | -ms-transform: scale(0); 488 | transform: scale(0); } 489 | 490 | 50% { 491 | -webkit-transform: scale(1.2); 492 | -moz-transform: scale(1.2); 493 | -ms-transform: scale(1.2); 494 | transform: scale(1.2); } 495 | 496 | 100% { 497 | -webkit-transform: scale(1); 498 | -moz-transform: scale(1); 499 | -ms-transform: scale(1); 500 | transform: scale(1); } } 501 | .tile-merged .tile-inner { 502 | z-index: 20; 503 | -webkit-animation: pop 200ms ease 100ms; 504 | -moz-animation: pop 200ms ease 100ms; 505 | animation: pop 200ms ease 100ms; 506 | -webkit-animation-fill-mode: backwards; 507 | -moz-animation-fill-mode: backwards; 508 | animation-fill-mode: backwards; } 509 | 510 | .above-game:after { 511 | content: ""; 512 | display: block; 513 | clear: both; } 514 | 515 | .game-intro { 516 | float: left; 517 | line-height: 42px; 518 | margin-bottom: 0; } 519 | 520 | .restart-button { 521 | display: inline-block; 522 | background: #8f7a66; 523 | border-radius: 3px; 524 | padding: 15px 25px; 525 | font-size: 25px; 526 | text-decoration: none; 527 | color: #f9f6f2; 528 | height: 25px; 529 | line-height: 47px; 530 | display: block; 531 | margin-top: 0px; 532 | margin-left:5px; 533 | text-align: center; 534 | float: right; } 535 | 536 | .game-explanation { 537 | margin-top: 50px; } 538 | 539 | @media screen and (max-width: 520px) { 540 | html, body { 541 | font-size: 15px; } 542 | 543 | body { 544 | margin: 0px 0; 545 | padding: 0 0px; } 546 | 547 | h1.title { 548 | font-size: 27px; 549 | margin-top: 15px; } 550 | 551 | .container { 552 | width: 280px; 553 | margin: 0 auto; } 554 | 555 | .score-container, .best-container { 556 | margin-top: 0; 557 | padding: 15px 10px; 558 | min-width: 40px; } 559 | 560 | .heading { 561 | margin-bottom: 0px; } 562 | 563 | .game-intro { 564 | width: 55%; 565 | display: block; 566 | box-sizing: border-box; 567 | line-height: 1.65; } 568 | 569 | .restart-button { 570 | width: 42%; 571 | padding: 0; 572 | display: block; 573 | box-sizing: border-box; 574 | margin-top: 0px; } 575 | 576 | .game-container { 577 | margin-top: 0px; 578 | position: relative; 579 | padding: 10px; 580 | cursor: default; 581 | -webkit-touch-callout: none; 582 | -ms-touch-callout: none; 583 | -webkit-user-select: none; 584 | -moz-user-select: none; 585 | -ms-user-select: none; 586 | -ms-touch-action: none; 587 | touch-action: none; 588 | background: #bbada0; 589 | border-radius: 6px; 590 | width: 280px; 591 | height: 280px; 592 | -webkit-box-sizing: border-box; 593 | -moz-box-sizing: border-box; 594 | box-sizing: border-box; } 595 | .game-container .game-message { 596 | display: none; 597 | position: absolute; 598 | top: 0; 599 | right: 0; 600 | bottom: 0; 601 | left: 0; 602 | background: rgba(238, 228, 218, 0.5); 603 | z-index: 100; 604 | text-align: center; 605 | -webkit-animation: fade-in 800ms ease 1200ms; 606 | -moz-animation: fade-in 800ms ease 1200ms; 607 | animation: fade-in 800ms ease 1200ms; 608 | -webkit-animation-fill-mode: both; 609 | -moz-animation-fill-mode: both; 610 | animation-fill-mode: both; } 611 | .game-container .game-message p { 612 | font-size: 60px; 613 | font-weight: bold; 614 | height: 60px; 615 | line-height: 60px; 616 | margin-top: 222px; } 617 | .game-container .game-message .lower { 618 | display: block; 619 | margin-top: 29px; } 620 | .game-container .game-message a { 621 | display: inline-block; 622 | background: #8f7a66; 623 | border-radius: 3px; 624 | padding: 0 20px; 625 | text-decoration: none; 626 | color: #f9f6f2; 627 | height: 40px; 628 | line-height: 42px; 629 | margin-left: 9px; } 630 | .game-container .game-message a.keep-playing-button { 631 | display: none; } 632 | .game-container .game-message.game-won { 633 | background: rgba(237, 194, 46, 0.5); 634 | color: #f9f6f2; } 635 | .game-container .game-message.game-won a.keep-playing-button { 636 | display: inline-block; } 637 | .game-container .game-message.game-won, .game-container .game-message.game-over { 638 | display: block; } 639 | 640 | .grid-container { 641 | position: absolute; 642 | z-index: 1; } 643 | 644 | .grid-row { 645 | margin-bottom: 10px; } 646 | .grid-row:last-child { 647 | margin-bottom: 0; } 648 | .grid-row:after { 649 | content: ""; 650 | display: block; 651 | clear: both; } 652 | 653 | .grid-cell { 654 | width: 57.5px; 655 | height: 57.5px; 656 | margin-right: 10px; 657 | float: left; 658 | border-radius: 3px; 659 | background: rgba(238, 228, 218, 0.35); } 660 | .grid-cell:last-child { 661 | margin-right: 0; } 662 | 663 | .tile-container { 664 | position: absolute; 665 | z-index: 2; } 666 | 667 | .tile, .tile .tile-inner { 668 | width: 58px; 669 | height: 58px; 670 | line-height: 58px; } 671 | .tile.tile-position-1-1 { 672 | -webkit-transform: translate(0px, 0px); 673 | -moz-transform: translate(0px, 0px); 674 | -ms-transform: translate(0px, 0px); 675 | transform: translate(0px, 0px); } 676 | .tile.tile-position-1-2 { 677 | -webkit-transform: translate(0px, 67px); 678 | -moz-transform: translate(0px, 67px); 679 | -ms-transform: translate(0px, 67px); 680 | transform: translate(0px, 67px); } 681 | .tile.tile-position-1-3 { 682 | -webkit-transform: translate(0px, 135px); 683 | -moz-transform: translate(0px, 135px); 684 | -ms-transform: translate(0px, 135px); 685 | transform: translate(0px, 135px); } 686 | .tile.tile-position-1-4 { 687 | -webkit-transform: translate(0px, 202px); 688 | -moz-transform: translate(0px, 202px); 689 | -ms-transform: translate(0px, 202px); 690 | transform: translate(0px, 202px); } 691 | .tile.tile-position-2-1 { 692 | -webkit-transform: translate(67px, 0px); 693 | -moz-transform: translate(67px, 0px); 694 | -ms-transform: translate(67px, 0px); 695 | transform: translate(67px, 0px); } 696 | .tile.tile-position-2-2 { 697 | -webkit-transform: translate(67px, 67px); 698 | -moz-transform: translate(67px, 67px); 699 | -ms-transform: translate(67px, 67px); 700 | transform: translate(67px, 67px); } 701 | .tile.tile-position-2-3 { 702 | -webkit-transform: translate(67px, 135px); 703 | -moz-transform: translate(67px, 135px); 704 | -ms-transform: translate(67px, 135px); 705 | transform: translate(67px, 135px); } 706 | .tile.tile-position-2-4 { 707 | -webkit-transform: translate(67px, 202px); 708 | -moz-transform: translate(67px, 202px); 709 | -ms-transform: translate(67px, 202px); 710 | transform: translate(67px, 202px); } 711 | .tile.tile-position-3-1 { 712 | -webkit-transform: translate(135px, 0px); 713 | -moz-transform: translate(135px, 0px); 714 | -ms-transform: translate(135px, 0px); 715 | transform: translate(135px, 0px); } 716 | .tile.tile-position-3-2 { 717 | -webkit-transform: translate(135px, 67px); 718 | -moz-transform: translate(135px, 67px); 719 | -ms-transform: translate(135px, 67px); 720 | transform: translate(135px, 67px); } 721 | .tile.tile-position-3-3 { 722 | -webkit-transform: translate(135px, 135px); 723 | -moz-transform: translate(135px, 135px); 724 | -ms-transform: translate(135px, 135px); 725 | transform: translate(135px, 135px); } 726 | .tile.tile-position-3-4 { 727 | -webkit-transform: translate(135px, 202px); 728 | -moz-transform: translate(135px, 202px); 729 | -ms-transform: translate(135px, 202px); 730 | transform: translate(135px, 202px); } 731 | .tile.tile-position-4-1 { 732 | -webkit-transform: translate(202px, 0px); 733 | -moz-transform: translate(202px, 0px); 734 | -ms-transform: translate(202px, 0px); 735 | transform: translate(202px, 0px); } 736 | .tile.tile-position-4-2 { 737 | -webkit-transform: translate(202px, 67px); 738 | -moz-transform: translate(202px, 67px); 739 | -ms-transform: translate(202px, 67px); 740 | transform: translate(202px, 67px); } 741 | .tile.tile-position-4-3 { 742 | -webkit-transform: translate(202px, 135px); 743 | -moz-transform: translate(202px, 135px); 744 | -ms-transform: translate(202px, 135px); 745 | transform: translate(202px, 135px); } 746 | .tile.tile-position-4-4 { 747 | -webkit-transform: translate(202px, 202px); 748 | -moz-transform: translate(202px, 202px); 749 | -ms-transform: translate(202px, 202px); 750 | transform: translate(202px, 202px); } 751 | 752 | .tile .tile-inner { 753 | font-size: 35px; } 754 | 755 | .game-message p { 756 | font-size: 30px !important; 757 | height: 30px !important; 758 | line-height: 30px !important; 759 | margin-top: 90px !important; } 760 | .game-message .lower { 761 | margin-top: 30px !important; } } 762 | -------------------------------------------------------------------------------- /style/fonts/ClearSans-Bold-webfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | -------------------------------------------------------------------------------- /style/fonts/ClearSans-Regular-webfont.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | --------------------------------------------------------------------------------