├── .gitignore ├── .jshintrc ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── Rakefile ├── favicon.ico ├── index.html ├── js ├── animframe_polyfill.js ├── application.js ├── bind_polyfill.js ├── classlist_polyfill.js ├── game_manager.js ├── grid.js ├── html_actuator.js ├── keyboard_input_manager.js ├── local_storage_manager.js └── tile.js ├── meta ├── apple-touch-icon.png ├── apple-touch-startup-image-640x1096.png ├── apple-touch-startup-image-640x920.png └── og.png └── style ├── fonts ├── ClearSans-Bold-webfont.eot ├── ClearSans-Bold-webfont.svg ├── ClearSans-Bold-webfont.woff ├── ClearSans-Light-webfont.eot ├── ClearSans-Light-webfont.svg ├── ClearSans-Light-webfont.woff ├── ClearSans-Regular-webfont.eot ├── ClearSans-Regular-webfont.svg ├── ClearSans-Regular-webfont.woff └── clear-sans.css └── main.css /.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache/ 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "indent": 2, 4 | "maxlen": 80, 5 | "freeze": true, 6 | "camelcase": true, 7 | "unused": true, 8 | "eqnull": true, 9 | "proto": true, 10 | "supernew": true, 11 | "noyield": true, 12 | "evil": true, 13 | "node": true, 14 | "boss": true, 15 | "expr": true, 16 | "loopfunc": true, 17 | "white": true, 18 | "maxdepth": 4 19 | } 20 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | Changes and improvements are more than welcome! Feel free to fork and open a pull request. 3 | 4 | Please follow the house rules to have a bigger chance of your contribution being merged. 5 | 6 | ## House rules 7 | 8 | ### How to make changes 9 | - To make changes, create a new branch based on `master` (do not create one from `gh-pages` unless strictly necessary) and make them there, then create a Pull Request to master. 10 | `gh-pages` is different from master in that it contains sharing features, analytics and other things that have no direct bearing with the game. `master` is the "pure" version of the game. 11 | - If you want to modify the CSS, please edit the SCSS files present in `style/`: `main.scss` and others. Don't edit the `main.css`, because it's supposed to be generated. 12 | In order to compile your SCSS modifications, you need to use the `sass` gem (install it by running `gem install sass` once Ruby is installed). 13 | To run SASS, simply use the following command: 14 | `sass --unix-newlines --watch style/main.scss` 15 | SASS will automatically recompile your css when changed. 16 | - `Rakefile` contains some tasks that help during development. Feel free to add useful tasks if needed. 17 | - Please use 2-space indentation when editing the JavaScript. A `.jshintrc` file is present, which will help your code to follow the guidelines if you install and run `jshint`. 18 | - Please test your modification thoroughly before submitting your Pull Request. 19 | 20 | ### Changes that might not be accepted 21 | We have to be conservative with the core game. This means that some modifications won't be merged, or will have to be evaluated carefully before being merged: 22 | 23 | - Undo/redo features 24 | - Save/reload features 25 | - Changes to how the tiles look or their contents 26 | - Changes to the layout 27 | - Changes to the grid size 28 | 29 | ### Changes that are welcome 30 | - Bug fixes 31 | - Compatibility improvements 32 | - "Under the hood" enhancements 33 | - Small changes that don't have an impact on the core gameplay 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Gabriele Cirulli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 併校模擬器 | Merge Simulator 2 | 3 | https://splitline.github.io/merge-simulator/ 4 | 5 | ## How To Play 6 | 併校刷國際排名! 7 | 8 | 不用當校長也能體驗到併校的快感。併得越多排名便會越高,試著刷到世界第一吧🐱 9 | 10 | > Fork From https://github.com/gabrielecirulli/2048/ 11 | 12 | --- 13 | 14 | 我的 commit message 好醜 = = 15 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "date" 2 | 3 | namespace :appcache do 4 | desc "update the date in the appcache file (in the gh-pages branch)" 5 | task :update do 6 | appcache = File.read("cache.appcache") 7 | updated = "# Updated: #{DateTime.now}" 8 | 9 | File.write("cache.appcache", appcache.sub(/^# Updated:.*$/, updated)) 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splitline/merge-simulator/HEAD/favicon.ico -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 17 | 18 | 19 | 併校模擬器 | Merge Simulator 20 | 21 | 22 | 23 | 24 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 42 | 43 | 44 | 45 |
46 |
47 |

併校模擬器

48 |
49 |
0
50 |
0
51 |
52 |
53 | 54 |
55 |

通通給我併校併起來!

56 | New Game 57 |
58 | 59 |
60 |
61 |

62 | 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 | How to play: 併校刷 QS 排名! 102 |

103 |
104 |

105 | Star 107 |

108 |
109 |

110 | Fork from gabrielecirulli/2048 111 |

112 |
113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 || { 55 | x: tile.x, 56 | y: tile.y 57 | }; 58 | var positionClass = this.positionClass(position); 59 | 60 | // We can't use classlist because it somehow glitches when replacing classes 61 | var classes = ["tile", "tile-" + tile.value, positionClass]; 62 | 63 | if (tile.value > 2048) classes.push("tile-super"); 64 | 65 | this.applyClasses(wrapper, classes); 66 | 67 | const mapping = { 68 | 2: "南台科大", 69 | 4: "中台科大", 70 | 8: "台北城市科大", 71 | 16: "台灣大學", 72 | 32: "台灣首府大學", 73 | 64: "亞洲大學", 74 | 128: "屏科", 75 | 256: "虎科", 76 | 512: "雲科", 77 | 1024: "台科", 78 | 2048: "開山科大" 79 | } 80 | 81 | inner.classList.add("tile-inner"); 82 | if (tile.value <= 2048) 83 | inner.textContent = mapping[tile.value]; 84 | else 85 | inner.textContent = "開山科大 lv." + (Math.log2(tile.value) - 10); 86 | 87 | if (tile.previousPosition) { 88 | // Make sure that the tile gets rendered in the previous position first 89 | window.requestAnimationFrame(function () { 90 | classes[2] = self.positionClass({ 91 | x: tile.x, 92 | y: tile.y 93 | }); 94 | self.applyClasses(wrapper, classes); // Update the position 95 | }); 96 | } else if (tile.mergedFrom) { 97 | classes.push("tile-merged"); 98 | this.applyClasses(wrapper, classes); 99 | 100 | // Render the tiles that merged 101 | tile.mergedFrom.forEach(function (merged) { 102 | self.addTile(merged); 103 | }); 104 | } else { 105 | classes.push("tile-new"); 106 | this.applyClasses(wrapper, classes); 107 | } 108 | 109 | // Add the inner part of the tile to the wrapper 110 | wrapper.appendChild(inner); 111 | 112 | // Put the tile on the board 113 | this.tileContainer.appendChild(wrapper); 114 | }; 115 | 116 | HTMLActuator.prototype.applyClasses = function (element, classes) { 117 | element.setAttribute("class", classes.join(" ")); 118 | }; 119 | 120 | HTMLActuator.prototype.normalizePosition = function (position) { 121 | return { 122 | x: position.x + 1, 123 | y: position.y + 1 124 | }; 125 | }; 126 | 127 | HTMLActuator.prototype.positionClass = function (position) { 128 | position = this.normalizePosition(position); 129 | return "tile-position-" + position.x + "-" + position.y; 130 | }; 131 | 132 | HTMLActuator.prototype.score2rank = function (score) { 133 | return Math.max(Math.floor( 134 | 1000 - (Math.min(score, 5000) * 0.1 + Math.max(score - 5000, 0) * 0.03) 135 | ), 1); 136 | } 137 | HTMLActuator.prototype.updateScore = function (score) { 138 | this.clearContainer(this.scoreContainer); 139 | 140 | var difference = this.score2rank(this.score) - this.score2rank(score); 141 | this.score = score; 142 | 143 | this.scoreContainer.textContent = this.score2rank(this.score) >= 1000 ? "1000+ 名" : this.score2rank(this.score) + "名"; 144 | 145 | if (difference > 0) { 146 | var addition = document.createElement("div"); 147 | addition.classList.add("score-addition"); 148 | addition.textContent = "⬆" + difference; 149 | 150 | this.scoreContainer.appendChild(addition); 151 | } 152 | }; 153 | 154 | HTMLActuator.prototype.updateBestScore = function (bestScore) { 155 | this.bestContainer.textContent = this.score2rank(this.score) >= 1000 ? "1000+ 名" : this.score2rank(this.score) + "名"; 156 | }; 157 | 158 | HTMLActuator.prototype.message = function (won) { 159 | var type = won ? "game-won" : "game-over"; 160 | var message = won ? "乂國立開山科大乂" : "垃圾學店下去888"; 161 | 162 | this.messageContainer.classList.add(type); 163 | this.messageContainer.getElementsByTagName("p")[0].textContent = message; 164 | }; 165 | 166 | HTMLActuator.prototype.clearMessage = function () { 167 | // IE only takes one value to remove at a time. 168 | this.messageContainer.classList.remove("game-won"); 169 | this.messageContainer.classList.remove("game-over"); 170 | }; -------------------------------------------------------------------------------- /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 | } 17 | 18 | KeyboardInputManager.prototype.on = function (event, callback) { 19 | if (!this.events[event]) { 20 | this.events[event] = []; 21 | } 22 | this.events[event].push(callback); 23 | }; 24 | 25 | KeyboardInputManager.prototype.emit = function (event, data) { 26 | var callbacks = this.events[event]; 27 | if (callbacks) { 28 | callbacks.forEach(function (callback) { 29 | callback(data); 30 | }); 31 | } 32 | }; 33 | 34 | KeyboardInputManager.prototype.listen = function () { 35 | var self = this; 36 | 37 | var map = { 38 | 38: 0, // Up 39 | 39: 1, // Right 40 | 40: 2, // Down 41 | 37: 3, // Left 42 | 75: 0, // Vim up 43 | 76: 1, // Vim right 44 | 74: 2, // Vim down 45 | 72: 3, // Vim left 46 | 87: 0, // W 47 | 68: 1, // D 48 | 83: 2, // S 49 | 65: 3 // A 50 | }; 51 | 52 | // Respond to direction keys 53 | document.addEventListener("keydown", function (event) { 54 | var modifiers = event.altKey || event.ctrlKey || event.metaKey || 55 | event.shiftKey; 56 | var mapped = map[event.which]; 57 | 58 | if (!modifiers) { 59 | if (mapped !== undefined) { 60 | event.preventDefault(); 61 | self.emit("move", mapped); 62 | } 63 | } 64 | 65 | // R key restarts the game 66 | if (!modifiers && event.which === 82) { 67 | self.restart.call(self, event); 68 | } 69 | }); 70 | 71 | // Respond to button presses 72 | this.bindButtonPress(".retry-button", this.restart); 73 | this.bindButtonPress(".restart-button", this.restart); 74 | this.bindButtonPress(".keep-playing-button", this.keepPlaying); 75 | 76 | // Respond to swipe events 77 | var touchStartClientX, touchStartClientY; 78 | var gameContainer = document.getElementsByClassName("game-container")[0]; 79 | 80 | gameContainer.addEventListener(this.eventTouchstart, function (event) { 81 | if ((!window.navigator.msPointerEnabled && event.touches.length > 1) || 82 | event.targetTouches.length > 1) { 83 | return; // Ignore if touching with more than 1 finger 84 | } 85 | 86 | if (window.navigator.msPointerEnabled) { 87 | touchStartClientX = event.pageX; 88 | touchStartClientY = event.pageY; 89 | } else { 90 | touchStartClientX = event.touches[0].clientX; 91 | touchStartClientY = event.touches[0].clientY; 92 | } 93 | 94 | event.preventDefault(); 95 | }); 96 | 97 | gameContainer.addEventListener(this.eventTouchmove, function (event) { 98 | event.preventDefault(); 99 | }); 100 | 101 | gameContainer.addEventListener(this.eventTouchend, function (event) { 102 | if ((!window.navigator.msPointerEnabled && event.touches.length > 0) || 103 | event.targetTouches.length > 0) { 104 | return; // Ignore if still touching with one or more fingers 105 | } 106 | 107 | var touchEndClientX, touchEndClientY; 108 | 109 | if (window.navigator.msPointerEnabled) { 110 | touchEndClientX = event.pageX; 111 | touchEndClientY = event.pageY; 112 | } else { 113 | touchEndClientX = event.changedTouches[0].clientX; 114 | touchEndClientY = event.changedTouches[0].clientY; 115 | } 116 | 117 | var dx = touchEndClientX - touchStartClientX; 118 | var absDx = Math.abs(dx); 119 | 120 | var dy = touchEndClientY - touchStartClientY; 121 | var absDy = Math.abs(dy); 122 | 123 | if (Math.max(absDx, absDy) > 10) { 124 | // (right : left) : (down : up) 125 | self.emit("move", absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0)); 126 | } 127 | }); 128 | }; 129 | 130 | KeyboardInputManager.prototype.restart = function (event) { 131 | event.preventDefault(); 132 | this.emit("restart"); 133 | }; 134 | 135 | KeyboardInputManager.prototype.keepPlaying = function (event) { 136 | event.preventDefault(); 137 | this.emit("keepPlaying"); 138 | }; 139 | 140 | KeyboardInputManager.prototype.bindButtonPress = function (selector, fn) { 141 | var button = document.querySelector(selector); 142 | button.addEventListener("click", fn.bind(this)); 143 | button.addEventListener(this.eventTouchend, fn.bind(this)); 144 | }; 145 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /meta/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splitline/merge-simulator/HEAD/meta/apple-touch-icon.png -------------------------------------------------------------------------------- /meta/apple-touch-startup-image-640x1096.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splitline/merge-simulator/HEAD/meta/apple-touch-startup-image-640x1096.png -------------------------------------------------------------------------------- /meta/apple-touch-startup-image-640x920.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splitline/merge-simulator/HEAD/meta/apple-touch-startup-image-640x920.png -------------------------------------------------------------------------------- /meta/og.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splitline/merge-simulator/HEAD/meta/og.png -------------------------------------------------------------------------------- /style/fonts/ClearSans-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splitline/merge-simulator/HEAD/style/fonts/ClearSans-Bold-webfont.eot -------------------------------------------------------------------------------- /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-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splitline/merge-simulator/HEAD/style/fonts/ClearSans-Bold-webfont.woff -------------------------------------------------------------------------------- /style/fonts/ClearSans-Light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splitline/merge-simulator/HEAD/style/fonts/ClearSans-Light-webfont.eot -------------------------------------------------------------------------------- /style/fonts/ClearSans-Light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splitline/merge-simulator/HEAD/style/fonts/ClearSans-Light-webfont.woff -------------------------------------------------------------------------------- /style/fonts/ClearSans-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splitline/merge-simulator/HEAD/style/fonts/ClearSans-Regular-webfont.eot -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /style/fonts/ClearSans-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/splitline/merge-simulator/HEAD/style/fonts/ClearSans-Regular-webfont.woff -------------------------------------------------------------------------------- /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?#iefix") format("embedded-opentype"), 5 | url("ClearSans-Light-webfont.svg#clear_sans_lightregular") 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?#iefix") format("embedded-opentype"), 15 | url("ClearSans-Regular-webfont.svg#clear_sansregular") 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?#iefix") format("embedded-opentype"), 25 | url("ClearSans-Bold-webfont.svg#clear_sansbold") format("svg"), 26 | url("ClearSans-Bold-webfont.woff") format("woff"); 27 | font-weight: 700; 28 | font-style: normal; 29 | } 30 | 31 | -------------------------------------------------------------------------------- /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: 80px 0; } 12 | 13 | .heading:after { 14 | content: ""; 15 | display: block; 16 | clear: both; } 17 | 18 | h1.title { 19 | font-size: 48px; 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: right; 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: 8px; 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: "QS 國際排名"; } 94 | 95 | .best-container:after { 96 | content: "最佳排名"; } 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: 15px; } 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: 35px; } 354 | @media screen and (max-width: 520px) { 355 | .tile.tile-128 .tile-inner { 356 | font-size: 10px; } } 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: 35px; } 362 | @media screen and (max-width: 520px) { 363 | .tile.tile-256 .tile-inner { 364 | font-size: 10px; } } 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: 35px; } 370 | @media screen and (max-width: 520px) { 371 | .tile.tile-512 .tile-inner { 372 | font-size: 10px; } } 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: 10px; } } 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: 40px; 386 | line-height: 52px;} 387 | @media screen and (max-width: 520px) { 388 | .tile.tile-2048 .tile-inner { 389 | font-size: 10px; } } 390 | .tile.tile-super .tile-inner { 391 | color: #f9f6f2; 392 | background: #3c3a32; 393 | font-size: 15px; } 394 | @media screen and (max-width: 520px) { 395 | .tile.tile-super .tile-inner { 396 | font-size: 10px; } } 397 | 398 | @-webkit-keyframes appear { 399 | 0% { 400 | opacity: 0; 401 | -webkit-transform: scale(0); 402 | -moz-transform: scale(0); 403 | -ms-transform: scale(0); 404 | transform: scale(0); } 405 | 406 | 100% { 407 | opacity: 1; 408 | -webkit-transform: scale(1); 409 | -moz-transform: scale(1); 410 | -ms-transform: scale(1); 411 | transform: scale(1); } } 412 | @-moz-keyframes appear { 413 | 0% { 414 | opacity: 0; 415 | -webkit-transform: scale(0); 416 | -moz-transform: scale(0); 417 | -ms-transform: scale(0); 418 | transform: scale(0); } 419 | 420 | 100% { 421 | opacity: 1; 422 | -webkit-transform: scale(1); 423 | -moz-transform: scale(1); 424 | -ms-transform: scale(1); 425 | transform: scale(1); } } 426 | @keyframes appear { 427 | 0% { 428 | opacity: 0; 429 | -webkit-transform: scale(0); 430 | -moz-transform: scale(0); 431 | -ms-transform: scale(0); 432 | transform: scale(0); } 433 | 434 | 100% { 435 | opacity: 1; 436 | -webkit-transform: scale(1); 437 | -moz-transform: scale(1); 438 | -ms-transform: scale(1); 439 | transform: scale(1); } } 440 | .tile-new .tile-inner { 441 | -webkit-animation: appear 200ms ease 100ms; 442 | -moz-animation: appear 200ms ease 100ms; 443 | animation: appear 200ms ease 100ms; 444 | -webkit-animation-fill-mode: backwards; 445 | -moz-animation-fill-mode: backwards; 446 | animation-fill-mode: backwards; } 447 | 448 | @-webkit-keyframes pop { 449 | 0% { 450 | -webkit-transform: scale(0); 451 | -moz-transform: scale(0); 452 | -ms-transform: scale(0); 453 | transform: scale(0); } 454 | 455 | 50% { 456 | -webkit-transform: scale(1.2); 457 | -moz-transform: scale(1.2); 458 | -ms-transform: scale(1.2); 459 | transform: scale(1.2); } 460 | 461 | 100% { 462 | -webkit-transform: scale(1); 463 | -moz-transform: scale(1); 464 | -ms-transform: scale(1); 465 | transform: scale(1); } } 466 | @-moz-keyframes pop { 467 | 0% { 468 | -webkit-transform: scale(0); 469 | -moz-transform: scale(0); 470 | -ms-transform: scale(0); 471 | transform: scale(0); } 472 | 473 | 50% { 474 | -webkit-transform: scale(1.2); 475 | -moz-transform: scale(1.2); 476 | -ms-transform: scale(1.2); 477 | transform: scale(1.2); } 478 | 479 | 100% { 480 | -webkit-transform: scale(1); 481 | -moz-transform: scale(1); 482 | -ms-transform: scale(1); 483 | transform: scale(1); } } 484 | @keyframes pop { 485 | 0% { 486 | -webkit-transform: scale(0); 487 | -moz-transform: scale(0); 488 | -ms-transform: scale(0); 489 | transform: scale(0); } 490 | 491 | 50% { 492 | -webkit-transform: scale(1.2); 493 | -moz-transform: scale(1.2); 494 | -ms-transform: scale(1.2); 495 | transform: scale(1.2); } 496 | 497 | 100% { 498 | -webkit-transform: scale(1); 499 | -moz-transform: scale(1); 500 | -ms-transform: scale(1); 501 | transform: scale(1); } } 502 | .tile-merged .tile-inner { 503 | z-index: 20; 504 | -webkit-animation: pop 200ms ease 100ms; 505 | -moz-animation: pop 200ms ease 100ms; 506 | animation: pop 200ms ease 100ms; 507 | -webkit-animation-fill-mode: backwards; 508 | -moz-animation-fill-mode: backwards; 509 | animation-fill-mode: backwards; } 510 | 511 | .above-game:after { 512 | content: ""; 513 | display: block; 514 | clear: both; } 515 | 516 | .game-intro { 517 | float: left; 518 | line-height: 42px; 519 | margin-bottom: 0; } 520 | 521 | .restart-button { 522 | display: inline-block; 523 | background: #8f7a66; 524 | border-radius: 3px; 525 | padding: 0 20px; 526 | text-decoration: none; 527 | color: #f9f6f2; 528 | height: 40px; 529 | line-height: 42px; 530 | display: block; 531 | text-align: center; 532 | float: right; } 533 | 534 | .game-explanation { 535 | margin-top: 50px; } 536 | 537 | @media screen and (max-width: 520px) { 538 | html, body { 539 | font-size: 15px; } 540 | 541 | body { 542 | margin: 20px 0; 543 | padding: 0 20px; } 544 | 545 | h1.title { 546 | font-size: 27px; 547 | margin-top: 15px; } 548 | 549 | .container { 550 | width: 280px; 551 | margin: 0 auto; } 552 | 553 | .score-container, .best-container { 554 | margin-top: 0; 555 | padding: 15px 10px; 556 | min-width: 40px; } 557 | 558 | .heading { 559 | margin-bottom: 10px; } 560 | 561 | .game-intro { 562 | width: 55%; 563 | display: block; 564 | box-sizing: border-box; 565 | line-height: 1.65; } 566 | 567 | .restart-button { 568 | width: 42%; 569 | padding: 0; 570 | display: block; 571 | box-sizing: border-box; 572 | margin-top: 2px; } 573 | 574 | .game-container { 575 | margin-top: 17px; 576 | position: relative; 577 | padding: 10px; 578 | cursor: default; 579 | -webkit-touch-callout: none; 580 | -ms-touch-callout: none; 581 | -webkit-user-select: none; 582 | -moz-user-select: none; 583 | -ms-user-select: none; 584 | -ms-touch-action: none; 585 | touch-action: none; 586 | background: #bbada0; 587 | border-radius: 6px; 588 | width: 280px; 589 | height: 280px; 590 | -webkit-box-sizing: border-box; 591 | -moz-box-sizing: border-box; 592 | box-sizing: border-box; } 593 | .game-container .game-message { 594 | display: none; 595 | position: absolute; 596 | top: 0; 597 | right: 0; 598 | bottom: 0; 599 | left: 0; 600 | background: rgba(238, 228, 218, 0.5); 601 | z-index: 100; 602 | text-align: center; 603 | -webkit-animation: fade-in 800ms ease 1200ms; 604 | -moz-animation: fade-in 800ms ease 1200ms; 605 | animation: fade-in 800ms ease 1200ms; 606 | -webkit-animation-fill-mode: both; 607 | -moz-animation-fill-mode: both; 608 | animation-fill-mode: both; } 609 | .game-container .game-message p { 610 | font-size: 60px; 611 | font-weight: bold; 612 | height: 60px; 613 | line-height: 60px; 614 | margin-top: 222px; } 615 | .game-container .game-message .lower { 616 | display: block; 617 | margin-top: 59px; } 618 | .game-container .game-message a { 619 | display: inline-block; 620 | background: #8f7a66; 621 | border-radius: 3px; 622 | padding: 0 20px; 623 | text-decoration: none; 624 | color: #f9f6f2; 625 | height: 40px; 626 | line-height: 42px; 627 | margin-left: 9px; } 628 | .game-container .game-message a.keep-playing-button { 629 | display: none; } 630 | .game-container .game-message.game-won { 631 | background: rgba(237, 194, 46, 0.5); 632 | color: #f9f6f2; } 633 | .game-container .game-message.game-won a.keep-playing-button { 634 | display: inline-block; } 635 | .game-container .game-message.game-won, .game-container .game-message.game-over { 636 | display: block; } 637 | 638 | .grid-container { 639 | position: absolute; 640 | z-index: 1; } 641 | 642 | .grid-row { 643 | margin-bottom: 10px; } 644 | .grid-row:last-child { 645 | margin-bottom: 0; } 646 | .grid-row:after { 647 | content: ""; 648 | display: block; 649 | clear: both; } 650 | 651 | .grid-cell { 652 | width: 57.5px; 653 | height: 57.5px; 654 | margin-right: 10px; 655 | float: left; 656 | border-radius: 3px; 657 | background: rgba(238, 228, 218, 0.35); } 658 | .grid-cell:last-child { 659 | margin-right: 0; } 660 | 661 | .tile-container { 662 | position: absolute; 663 | z-index: 2; } 664 | 665 | .tile, .tile .tile-inner { 666 | width: 58px; 667 | height: 58px; 668 | line-height: 58px; } 669 | .tile.tile-position-1-1 { 670 | -webkit-transform: translate(0px, 0px); 671 | -moz-transform: translate(0px, 0px); 672 | -ms-transform: translate(0px, 0px); 673 | transform: translate(0px, 0px); } 674 | .tile.tile-position-1-2 { 675 | -webkit-transform: translate(0px, 67px); 676 | -moz-transform: translate(0px, 67px); 677 | -ms-transform: translate(0px, 67px); 678 | transform: translate(0px, 67px); } 679 | .tile.tile-position-1-3 { 680 | -webkit-transform: translate(0px, 135px); 681 | -moz-transform: translate(0px, 135px); 682 | -ms-transform: translate(0px, 135px); 683 | transform: translate(0px, 135px); } 684 | .tile.tile-position-1-4 { 685 | -webkit-transform: translate(0px, 202px); 686 | -moz-transform: translate(0px, 202px); 687 | -ms-transform: translate(0px, 202px); 688 | transform: translate(0px, 202px); } 689 | .tile.tile-position-2-1 { 690 | -webkit-transform: translate(67px, 0px); 691 | -moz-transform: translate(67px, 0px); 692 | -ms-transform: translate(67px, 0px); 693 | transform: translate(67px, 0px); } 694 | .tile.tile-position-2-2 { 695 | -webkit-transform: translate(67px, 67px); 696 | -moz-transform: translate(67px, 67px); 697 | -ms-transform: translate(67px, 67px); 698 | transform: translate(67px, 67px); } 699 | .tile.tile-position-2-3 { 700 | -webkit-transform: translate(67px, 135px); 701 | -moz-transform: translate(67px, 135px); 702 | -ms-transform: translate(67px, 135px); 703 | transform: translate(67px, 135px); } 704 | .tile.tile-position-2-4 { 705 | -webkit-transform: translate(67px, 202px); 706 | -moz-transform: translate(67px, 202px); 707 | -ms-transform: translate(67px, 202px); 708 | transform: translate(67px, 202px); } 709 | .tile.tile-position-3-1 { 710 | -webkit-transform: translate(135px, 0px); 711 | -moz-transform: translate(135px, 0px); 712 | -ms-transform: translate(135px, 0px); 713 | transform: translate(135px, 0px); } 714 | .tile.tile-position-3-2 { 715 | -webkit-transform: translate(135px, 67px); 716 | -moz-transform: translate(135px, 67px); 717 | -ms-transform: translate(135px, 67px); 718 | transform: translate(135px, 67px); } 719 | .tile.tile-position-3-3 { 720 | -webkit-transform: translate(135px, 135px); 721 | -moz-transform: translate(135px, 135px); 722 | -ms-transform: translate(135px, 135px); 723 | transform: translate(135px, 135px); } 724 | .tile.tile-position-3-4 { 725 | -webkit-transform: translate(135px, 202px); 726 | -moz-transform: translate(135px, 202px); 727 | -ms-transform: translate(135px, 202px); 728 | transform: translate(135px, 202px); } 729 | .tile.tile-position-4-1 { 730 | -webkit-transform: translate(202px, 0px); 731 | -moz-transform: translate(202px, 0px); 732 | -ms-transform: translate(202px, 0px); 733 | transform: translate(202px, 0px); } 734 | .tile.tile-position-4-2 { 735 | -webkit-transform: translate(202px, 67px); 736 | -moz-transform: translate(202px, 67px); 737 | -ms-transform: translate(202px, 67px); 738 | transform: translate(202px, 67px); } 739 | .tile.tile-position-4-3 { 740 | -webkit-transform: translate(202px, 135px); 741 | -moz-transform: translate(202px, 135px); 742 | -ms-transform: translate(202px, 135px); 743 | transform: translate(202px, 135px); } 744 | .tile.tile-position-4-4 { 745 | -webkit-transform: translate(202px, 202px); 746 | -moz-transform: translate(202px, 202px); 747 | -ms-transform: translate(202px, 202px); 748 | transform: translate(202px, 202px); } 749 | 750 | .tile .tile-inner { 751 | font-size: 10px; } 752 | 753 | .game-message p { 754 | font-size: 30px !important; 755 | height: 30px !important; 756 | line-height: 30px !important; 757 | margin-top: 90px !important; } 758 | .game-message .lower { 759 | margin-top: 30px !important; } } --------------------------------------------------------------------------------