From 0b92a1411cef18d7e33f1879da70486821cb963e Mon Sep 17 00:00:00 2001
From: gfdgd_xi <3025613752@qq.com>
Date: Wed, 19 Apr 2023 21:57:17 +0800
Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A02048=E5=BD=A9=E8=9B=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
2048/.gitignore | 1 +
2048/.jshintrc | 19 +
2048/CONTRIBUTING.md | 33 +
2048/LICENSE.txt | 21 +
2048/README.md | 38 +
2048/Rakefile | 11 +
2048/favicon.ico | Bin 0 -> 4286 bytes
2048/index.html | 99 +++
2048/js/animframe_polyfill.js | 28 +
2048/js/application.js | 4 +
2048/js/bind_polyfill.js | 9 +
2048/js/classlist_polyfill.js | 71 ++
2048/js/game_manager.js | 272 +++++++
2048/js/grid.js | 117 +++
2048/js/html_actuator.js | 139 ++++
2048/js/keyboard_input_manager.js | 144 ++++
2048/js/local_storage_manager.js | 63 ++
2048/js/tile.js | 27 +
2048/meta/apple-touch-icon.png | Bin 0 -> 5808 bytes
.../apple-touch-startup-image-640x1096.png | Bin 0 -> 70707 bytes
.../apple-touch-startup-image-640x920.png | Bin 0 -> 42020 bytes
2048/style/fonts/ClearSans-Bold-webfont.eot | Bin 0 -> 23821 bytes
2048/style/fonts/ClearSans-Bold-webfont.svg | 640 +++++++++++++++
2048/style/fonts/ClearSans-Bold-webfont.woff | Bin 0 -> 27120 bytes
2048/style/fonts/ClearSans-Light-webfont.eot | Bin 0 -> 25570 bytes
2048/style/fonts/ClearSans-Light-webfont.svg | 670 ++++++++++++++++
2048/style/fonts/ClearSans-Light-webfont.woff | Bin 0 -> 28700 bytes
.../style/fonts/ClearSans-Regular-webfont.eot | Bin 0 -> 23490 bytes
.../style/fonts/ClearSans-Regular-webfont.svg | 669 ++++++++++++++++
.../fonts/ClearSans-Regular-webfont.woff | Bin 0 -> 26764 bytes
2048/style/fonts/clear-sans.css | 30 +
2048/style/helpers.scss | 82 ++
2048/style/main.css | 758 ++++++++++++++++++
2048/style/main.scss | 549 +++++++++++++
34 files changed, 4494 insertions(+)
create mode 100644 2048/.gitignore
create mode 100644 2048/.jshintrc
create mode 100644 2048/CONTRIBUTING.md
create mode 100644 2048/LICENSE.txt
create mode 100644 2048/README.md
create mode 100644 2048/Rakefile
create mode 100644 2048/favicon.ico
create mode 100644 2048/index.html
create mode 100644 2048/js/animframe_polyfill.js
create mode 100644 2048/js/application.js
create mode 100644 2048/js/bind_polyfill.js
create mode 100644 2048/js/classlist_polyfill.js
create mode 100644 2048/js/game_manager.js
create mode 100644 2048/js/grid.js
create mode 100644 2048/js/html_actuator.js
create mode 100644 2048/js/keyboard_input_manager.js
create mode 100644 2048/js/local_storage_manager.js
create mode 100644 2048/js/tile.js
create mode 100644 2048/meta/apple-touch-icon.png
create mode 100644 2048/meta/apple-touch-startup-image-640x1096.png
create mode 100644 2048/meta/apple-touch-startup-image-640x920.png
create mode 100755 2048/style/fonts/ClearSans-Bold-webfont.eot
create mode 100755 2048/style/fonts/ClearSans-Bold-webfont.svg
create mode 100755 2048/style/fonts/ClearSans-Bold-webfont.woff
create mode 100755 2048/style/fonts/ClearSans-Light-webfont.eot
create mode 100755 2048/style/fonts/ClearSans-Light-webfont.svg
create mode 100755 2048/style/fonts/ClearSans-Light-webfont.woff
create mode 100755 2048/style/fonts/ClearSans-Regular-webfont.eot
create mode 100755 2048/style/fonts/ClearSans-Regular-webfont.svg
create mode 100755 2048/style/fonts/ClearSans-Regular-webfont.woff
create mode 100755 2048/style/fonts/clear-sans.css
create mode 100644 2048/style/helpers.scss
create mode 100644 2048/style/main.css
create mode 100644 2048/style/main.scss
diff --git a/2048/.gitignore b/2048/.gitignore
new file mode 100644
index 0000000..0d31019
--- /dev/null
+++ b/2048/.gitignore
@@ -0,0 +1 @@
+.sass-cache/
diff --git a/2048/.jshintrc b/2048/.jshintrc
new file mode 100644
index 0000000..87dbf9a
--- /dev/null
+++ b/2048/.jshintrc
@@ -0,0 +1,19 @@
+{
+ "esnext": true,
+ "indent": 2,
+ "maxlen": 80,
+ "freeze": true,
+ "camelcase": true,
+ "unused": true,
+ "eqnull": true,
+ "proto": true,
+ "supernew": true,
+ "noyield": true,
+ "evil": true,
+ "node": true,
+ "boss": true,
+ "expr": true,
+ "loopfunc": true,
+ "white": true,
+ "maxdepth": 4
+}
diff --git a/2048/CONTRIBUTING.md b/2048/CONTRIBUTING.md
new file mode 100644
index 0000000..8b269ae
--- /dev/null
+++ b/2048/CONTRIBUTING.md
@@ -0,0 +1,33 @@
+# Contributing
+Changes and improvements are more than welcome! Feel free to fork and open a pull request.
+
+Please follow the house rules to have a bigger chance of your contribution being merged.
+
+## House rules
+
+### How to make changes
+ - 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.
+ `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.
+ - 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.
+ 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).
+ To run SASS, simply use the following command:
+ `sass --unix-newlines --watch style/main.scss`
+ SASS will automatically recompile your css when changed.
+ - `Rakefile` contains some tasks that help during development. Feel free to add useful tasks if needed.
+ - 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`.
+ - Please test your modification thoroughly before submitting your Pull Request.
+
+### Changes that might not be accepted
+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:
+
+ - Undo/redo features
+ - Save/reload features
+ - Changes to how the tiles look or their contents
+ - Changes to the layout
+ - Changes to the grid size
+
+### Changes that are welcome
+ - Bug fixes
+ - Compatibility improvements
+ - "Under the hood" enhancements
+ - Small changes that don't have an impact on the core gameplay
diff --git a/2048/LICENSE.txt b/2048/LICENSE.txt
new file mode 100644
index 0000000..b0dbfa4
--- /dev/null
+++ b/2048/LICENSE.txt
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Gabriele Cirulli
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/2048/README.md b/2048/README.md
new file mode 100644
index 0000000..6b1da38
--- /dev/null
+++ b/2048/README.md
@@ -0,0 +1,38 @@
+# 2048
+A small clone of [1024](https://play.google.com/store/apps/details?id=com.veewo.a1024), based on [Saming's 2048](http://saming.fr/p/2048/) (also a clone).
+
+Made just for fun. [Play it here!](http://gabrielecirulli.github.io/2048/)
+
+The official app can also be found on the [Play Store](https://play.google.com/store/apps/details?id=com.gabrielecirulli.app2048) and [App Store!](https://itunes.apple.com/us/app/2048-by-gabriele-cirulli/id868076805)
+
+### Contributions
+
+[Anna Harren](https://github.com/iirelu/) and [sigod](https://github.com/sigod) are maintainers for this repository.
+
+Other notable contributors:
+
+ - [TimPetricola](https://github.com/TimPetricola) added best score storage
+ - [chrisprice](https://github.com/chrisprice) added custom code for swipe handling on mobile
+ - [marcingajda](https://github.com/marcingajda) made swipes work on Windows Phone
+ - [mgarciaisaia](https://github.com/mgarciaisaia) added support for Android 2.3
+
+Many thanks to [rayhaanj](https://github.com/rayhaanj), [Mechazawa](https://github.com/Mechazawa), [grant](https://github.com/grant), [remram44](https://github.com/remram44) and [ghoullier](https://github.com/ghoullier) for the many other good contributions.
+
+### Screenshot
+
+
+
+
+
+That screenshot is fake, by the way. I never reached 2048 :smile:
+
+## Contributing
+Changes and improvements are more than welcome! Feel free to fork and open a pull request. Please make your changes in a specific branch and request to pull into `master`! If you can, please make sure the game fully works before sending the PR, as that will help speed up the process.
+
+You can find the same information in the [contributing guide.](https://github.com/gabrielecirulli/2048/blob/master/CONTRIBUTING.md)
+
+## License
+2048 is licensed under the [MIT license.](https://github.com/gabrielecirulli/2048/blob/master/LICENSE.txt)
+
+## Donations
+I made this in my spare time, and it's hosted on GitHub (which means I don't have any hosting costs), but if you enjoyed the game and feel like buying me coffee, you can donate at my BTC address: `1Ec6onfsQmoP9kkL3zkpB6c5sA4PVcXU2i`. Thank you very much!
diff --git a/2048/Rakefile b/2048/Rakefile
new file mode 100644
index 0000000..3e9851e
--- /dev/null
+++ b/2048/Rakefile
@@ -0,0 +1,11 @@
+require "date"
+
+namespace :appcache do
+ desc "update the date in the appcache file (in the gh-pages branch)"
+ task :update do
+ appcache = File.read("cache.appcache")
+ updated = "# Updated: #{DateTime.now}"
+
+ File.write("cache.appcache", appcache.sub(/^# Updated:.*$/, updated))
+ end
+end
diff --git a/2048/favicon.ico b/2048/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..22109e04a9f44bde18ec7b7a4b7410d0246521bc
GIT binary patch
literal 4286
zcmeI0u}T9$5QaxAZ4|*eSlRmm7M9V@SMU{VQ>5?}d;{?n1WA#?B1H-d3kAW#!e}&!
z290>`cj4E~-Q@zpx`i3p?A^})&p$J}fvjc=?ypiYKJ9qfY|YFz+;B^_
zFn$f<2q!J!u7@9~i@DVw36{n#zAi#L=w1qYr#f#FwA?@S$6_fB4AP=j)up
z&-xU9K1cfvmODrDz4%vq5%c#q+Oh8+)rjYPd=HIz|I|hJ=PnrXPDE_>yDHg+*KulwNh1`
literal 0
HcmV?d00001
diff --git a/2048/index.html b/2048/index.html
new file mode 100644
index 0000000..0da0ee0
--- /dev/null
+++ b/2048/index.html
@@ -0,0 +1,99 @@
+
+
+
+
+ 2048
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Join the numbers and get to the 2048 tile!
+
New Game
+
+
+
+
+
+ How to play: Use your arrow keys to move the tiles. When two tiles with the same number touch, they merge into one!
+
+
+
+ Note: This site is the official version of 2048. You can play it on your phone via http://git.io/2048. All other apps or sites are derivatives or fakes, and should be used with caution.
+
+
+
+ Created by Gabriele Cirulli. Based on 1024 by Veewo Studio and conceptually similar to Threes by Asher Vollmer.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/2048/js/animframe_polyfill.js b/2048/js/animframe_polyfill.js
new file mode 100644
index 0000000..c524a99
--- /dev/null
+++ b/2048/js/animframe_polyfill.js
@@ -0,0 +1,28 @@
+(function () {
+ var lastTime = 0;
+ var vendors = ['webkit', 'moz'];
+ for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
+ window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
+ window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||
+ window[vendors[x] + 'CancelRequestAnimationFrame'];
+ }
+
+ if (!window.requestAnimationFrame) {
+ window.requestAnimationFrame = function (callback) {
+ var currTime = new Date().getTime();
+ var timeToCall = Math.max(0, 16 - (currTime - lastTime));
+ var id = window.setTimeout(function () {
+ callback(currTime + timeToCall);
+ },
+ timeToCall);
+ lastTime = currTime + timeToCall;
+ return id;
+ };
+ }
+
+ if (!window.cancelAnimationFrame) {
+ window.cancelAnimationFrame = function (id) {
+ clearTimeout(id);
+ };
+ }
+}());
diff --git a/2048/js/application.js b/2048/js/application.js
new file mode 100644
index 0000000..2c1108e
--- /dev/null
+++ b/2048/js/application.js
@@ -0,0 +1,4 @@
+// Wait till the browser is ready to render the game (avoids glitches)
+window.requestAnimationFrame(function () {
+ new GameManager(4, KeyboardInputManager, HTMLActuator, LocalStorageManager);
+});
diff --git a/2048/js/bind_polyfill.js b/2048/js/bind_polyfill.js
new file mode 100644
index 0000000..8d9c4a4
--- /dev/null
+++ b/2048/js/bind_polyfill.js
@@ -0,0 +1,9 @@
+Function.prototype.bind = Function.prototype.bind || function (target) {
+ var self = this;
+ return function (args) {
+ if (!(args instanceof Array)) {
+ args = [args];
+ }
+ self.apply(target, args);
+ };
+};
diff --git a/2048/js/classlist_polyfill.js b/2048/js/classlist_polyfill.js
new file mode 100644
index 0000000..1789ae7
--- /dev/null
+++ b/2048/js/classlist_polyfill.js
@@ -0,0 +1,71 @@
+(function () {
+ if (typeof window.Element === "undefined" ||
+ "classList" in document.documentElement) {
+ return;
+ }
+
+ var prototype = Array.prototype,
+ push = prototype.push,
+ splice = prototype.splice,
+ join = prototype.join;
+
+ function DOMTokenList(el) {
+ this.el = el;
+ // The className needs to be trimmed and split on whitespace
+ // to retrieve a list of classes.
+ var classes = el.className.replace(/^\s+|\s+$/g, '').split(/\s+/);
+ for (var i = 0; i < classes.length; i++) {
+ push.call(this, classes[i]);
+ }
+ }
+
+ DOMTokenList.prototype = {
+ add: function (token) {
+ if (this.contains(token)) return;
+ push.call(this, token);
+ this.el.className = this.toString();
+ },
+ contains: function (token) {
+ return this.el.className.indexOf(token) != -1;
+ },
+ item: function (index) {
+ return this[index] || null;
+ },
+ remove: function (token) {
+ if (!this.contains(token)) return;
+ for (var i = 0; i < this.length; i++) {
+ if (this[i] == token) break;
+ }
+ splice.call(this, i, 1);
+ this.el.className = this.toString();
+ },
+ toString: function () {
+ return join.call(this, ' ');
+ },
+ toggle: function (token) {
+ if (!this.contains(token)) {
+ this.add(token);
+ } else {
+ this.remove(token);
+ }
+
+ return this.contains(token);
+ }
+ };
+
+ window.DOMTokenList = DOMTokenList;
+
+ function defineElementGetter(obj, prop, getter) {
+ if (Object.defineProperty) {
+ Object.defineProperty(obj, prop, {
+ get: getter
+ });
+ } else {
+ obj.__defineGetter__(prop, getter);
+ }
+ }
+
+ defineElementGetter(HTMLElement.prototype, 'classList', function () {
+ return new DOMTokenList(this);
+ });
+})();
diff --git a/2048/js/game_manager.js b/2048/js/game_manager.js
new file mode 100644
index 0000000..1c13d15
--- /dev/null
+++ b/2048/js/game_manager.js
@@ -0,0 +1,272 @@
+function GameManager(size, InputManager, Actuator, StorageManager) {
+ this.size = size; // Size of the grid
+ this.inputManager = new InputManager;
+ this.storageManager = new StorageManager;
+ this.actuator = new Actuator;
+
+ this.startTiles = 2;
+
+ this.inputManager.on("move", this.move.bind(this));
+ this.inputManager.on("restart", this.restart.bind(this));
+ this.inputManager.on("keepPlaying", this.keepPlaying.bind(this));
+
+ this.setup();
+}
+
+// Restart the game
+GameManager.prototype.restart = function () {
+ this.storageManager.clearGameState();
+ this.actuator.continueGame(); // Clear the game won/lost message
+ this.setup();
+};
+
+// Keep playing after winning (allows going over 2048)
+GameManager.prototype.keepPlaying = function () {
+ this.keepPlaying = true;
+ this.actuator.continueGame(); // Clear the game won/lost message
+};
+
+// Return true if the game is lost, or has won and the user hasn't kept playing
+GameManager.prototype.isGameTerminated = function () {
+ return this.over || (this.won && !this.keepPlaying);
+};
+
+// Set up the game
+GameManager.prototype.setup = function () {
+ var previousState = this.storageManager.getGameState();
+
+ // Reload the game from a previous game if present
+ if (previousState) {
+ this.grid = new Grid(previousState.grid.size,
+ previousState.grid.cells); // Reload grid
+ this.score = previousState.score;
+ this.over = previousState.over;
+ this.won = previousState.won;
+ this.keepPlaying = previousState.keepPlaying;
+ } else {
+ this.grid = new Grid(this.size);
+ this.score = 0;
+ this.over = false;
+ this.won = false;
+ this.keepPlaying = false;
+
+ // Add the initial tiles
+ this.addStartTiles();
+ }
+
+ // Update the actuator
+ this.actuate();
+};
+
+// Set up the initial tiles to start the game with
+GameManager.prototype.addStartTiles = function () {
+ for (var i = 0; i < this.startTiles; i++) {
+ this.addRandomTile();
+ }
+};
+
+// Adds a tile in a random position
+GameManager.prototype.addRandomTile = function () {
+ if (this.grid.cellsAvailable()) {
+ var value = Math.random() < 0.9 ? 2 : 4;
+ var tile = new Tile(this.grid.randomAvailableCell(), value);
+
+ this.grid.insertTile(tile);
+ }
+};
+
+// Sends the updated grid to the actuator
+GameManager.prototype.actuate = function () {
+ if (this.storageManager.getBestScore() < this.score) {
+ this.storageManager.setBestScore(this.score);
+ }
+
+ // Clear the state when the game is over (game over only, not win)
+ if (this.over) {
+ this.storageManager.clearGameState();
+ } else {
+ this.storageManager.setGameState(this.serialize());
+ }
+
+ this.actuator.actuate(this.grid, {
+ score: this.score,
+ over: this.over,
+ won: this.won,
+ bestScore: this.storageManager.getBestScore(),
+ terminated: this.isGameTerminated()
+ });
+
+};
+
+// Represent the current game as an object
+GameManager.prototype.serialize = function () {
+ return {
+ grid: this.grid.serialize(),
+ score: this.score,
+ over: this.over,
+ won: this.won,
+ keepPlaying: this.keepPlaying
+ };
+};
+
+// Save all tile positions and remove merger info
+GameManager.prototype.prepareTiles = function () {
+ this.grid.eachCell(function (x, y, tile) {
+ if (tile) {
+ tile.mergedFrom = null;
+ tile.savePosition();
+ }
+ });
+};
+
+// Move a tile and its representation
+GameManager.prototype.moveTile = function (tile, cell) {
+ this.grid.cells[tile.x][tile.y] = null;
+ this.grid.cells[cell.x][cell.y] = tile;
+ tile.updatePosition(cell);
+};
+
+// Move tiles on the grid in the specified direction
+GameManager.prototype.move = function (direction) {
+ // 0: up, 1: right, 2: down, 3: left
+ var self = this;
+
+ if (this.isGameTerminated()) return; // Don't do anything if the game's over
+
+ var cell, tile;
+
+ var vector = this.getVector(direction);
+ var traversals = this.buildTraversals(vector);
+ var moved = false;
+
+ // Save the current tile positions and remove merger information
+ this.prepareTiles();
+
+ // Traverse the grid in the right direction and move tiles
+ traversals.x.forEach(function (x) {
+ traversals.y.forEach(function (y) {
+ cell = { x: x, y: y };
+ tile = self.grid.cellContent(cell);
+
+ if (tile) {
+ var positions = self.findFarthestPosition(cell, vector);
+ var next = self.grid.cellContent(positions.next);
+
+ // Only one merger per row traversal?
+ if (next && next.value === tile.value && !next.mergedFrom) {
+ var merged = new Tile(positions.next, tile.value * 2);
+ merged.mergedFrom = [tile, next];
+
+ self.grid.insertTile(merged);
+ self.grid.removeTile(tile);
+
+ // Converge the two tiles' positions
+ tile.updatePosition(positions.next);
+
+ // Update the score
+ self.score += merged.value;
+
+ // The mighty 2048 tile
+ if (merged.value === 2048) self.won = true;
+ } else {
+ self.moveTile(tile, positions.farthest);
+ }
+
+ if (!self.positionsEqual(cell, tile)) {
+ moved = true; // The tile moved from its original cell!
+ }
+ }
+ });
+ });
+
+ if (moved) {
+ this.addRandomTile();
+
+ if (!this.movesAvailable()) {
+ this.over = true; // Game over!
+ }
+
+ this.actuate();
+ }
+};
+
+// Get the vector representing the chosen direction
+GameManager.prototype.getVector = function (direction) {
+ // Vectors representing tile movement
+ var map = {
+ 0: { x: 0, y: -1 }, // Up
+ 1: { x: 1, y: 0 }, // Right
+ 2: { x: 0, y: 1 }, // Down
+ 3: { x: -1, y: 0 } // Left
+ };
+
+ return map[direction];
+};
+
+// Build a list of positions to traverse in the right order
+GameManager.prototype.buildTraversals = function (vector) {
+ var traversals = { x: [], y: [] };
+
+ for (var pos = 0; pos < this.size; pos++) {
+ traversals.x.push(pos);
+ traversals.y.push(pos);
+ }
+
+ // Always traverse from the farthest cell in the chosen direction
+ if (vector.x === 1) traversals.x = traversals.x.reverse();
+ if (vector.y === 1) traversals.y = traversals.y.reverse();
+
+ return traversals;
+};
+
+GameManager.prototype.findFarthestPosition = function (cell, vector) {
+ var previous;
+
+ // Progress towards the vector direction until an obstacle is found
+ do {
+ previous = cell;
+ cell = { x: previous.x + vector.x, y: previous.y + vector.y };
+ } while (this.grid.withinBounds(cell) &&
+ this.grid.cellAvailable(cell));
+
+ return {
+ farthest: previous,
+ next: cell // Used to check if a merge is required
+ };
+};
+
+GameManager.prototype.movesAvailable = function () {
+ return this.grid.cellsAvailable() || this.tileMatchesAvailable();
+};
+
+// Check for available matches between tiles (more expensive check)
+GameManager.prototype.tileMatchesAvailable = function () {
+ var self = this;
+
+ var tile;
+
+ for (var x = 0; x < this.size; x++) {
+ for (var y = 0; y < this.size; y++) {
+ tile = this.grid.cellContent({ x: x, y: y });
+
+ if (tile) {
+ for (var direction = 0; direction < 4; direction++) {
+ var vector = self.getVector(direction);
+ var cell = { x: x + vector.x, y: y + vector.y };
+
+ var other = self.grid.cellContent(cell);
+
+ if (other && other.value === tile.value) {
+ return true; // These two tiles can be merged
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+};
+
+GameManager.prototype.positionsEqual = function (first, second) {
+ return first.x === second.x && first.y === second.y;
+};
diff --git a/2048/js/grid.js b/2048/js/grid.js
new file mode 100644
index 0000000..29f0821
--- /dev/null
+++ b/2048/js/grid.js
@@ -0,0 +1,117 @@
+function Grid(size, previousState) {
+ this.size = size;
+ this.cells = previousState ? this.fromState(previousState) : this.empty();
+}
+
+// Build a grid of the specified size
+Grid.prototype.empty = function () {
+ var cells = [];
+
+ for (var x = 0; x < this.size; x++) {
+ var row = cells[x] = [];
+
+ for (var y = 0; y < this.size; y++) {
+ row.push(null);
+ }
+ }
+
+ return cells;
+};
+
+Grid.prototype.fromState = function (state) {
+ var cells = [];
+
+ for (var x = 0; x < this.size; x++) {
+ var row = cells[x] = [];
+
+ for (var y = 0; y < this.size; y++) {
+ var tile = state[x][y];
+ row.push(tile ? new Tile(tile.position, tile.value) : null);
+ }
+ }
+
+ return cells;
+};
+
+// Find the first available random position
+Grid.prototype.randomAvailableCell = function () {
+ var cells = this.availableCells();
+
+ if (cells.length) {
+ return cells[Math.floor(Math.random() * cells.length)];
+ }
+};
+
+Grid.prototype.availableCells = function () {
+ var cells = [];
+
+ this.eachCell(function (x, y, tile) {
+ if (!tile) {
+ cells.push({ x: x, y: y });
+ }
+ });
+
+ return cells;
+};
+
+// Call callback for every cell
+Grid.prototype.eachCell = function (callback) {
+ for (var x = 0; x < this.size; x++) {
+ for (var y = 0; y < this.size; y++) {
+ callback(x, y, this.cells[x][y]);
+ }
+ }
+};
+
+// Check if there are any cells available
+Grid.prototype.cellsAvailable = function () {
+ return !!this.availableCells().length;
+};
+
+// Check if the specified cell is taken
+Grid.prototype.cellAvailable = function (cell) {
+ return !this.cellOccupied(cell);
+};
+
+Grid.prototype.cellOccupied = function (cell) {
+ return !!this.cellContent(cell);
+};
+
+Grid.prototype.cellContent = function (cell) {
+ if (this.withinBounds(cell)) {
+ return this.cells[cell.x][cell.y];
+ } else {
+ return null;
+ }
+};
+
+// Inserts a tile at its position
+Grid.prototype.insertTile = function (tile) {
+ this.cells[tile.x][tile.y] = tile;
+};
+
+Grid.prototype.removeTile = function (tile) {
+ this.cells[tile.x][tile.y] = null;
+};
+
+Grid.prototype.withinBounds = function (position) {
+ return position.x >= 0 && position.x < this.size &&
+ position.y >= 0 && position.y < this.size;
+};
+
+Grid.prototype.serialize = function () {
+ var cellState = [];
+
+ for (var x = 0; x < this.size; x++) {
+ var row = cellState[x] = [];
+
+ for (var y = 0; y < this.size; y++) {
+ row.push(this.cells[x][y] ? this.cells[x][y].serialize() : null);
+ }
+ }
+
+ return {
+ size: this.size,
+ cells: cellState
+ };
+};
diff --git a/2048/js/html_actuator.js b/2048/js/html_actuator.js
new file mode 100644
index 0000000..6b31f2d
--- /dev/null
+++ b/2048/js/html_actuator.js
@@ -0,0 +1,139 @@
+function HTMLActuator() {
+ this.tileContainer = document.querySelector(".tile-container");
+ this.scoreContainer = document.querySelector(".score-container");
+ this.bestContainer = document.querySelector(".best-container");
+ this.messageContainer = document.querySelector(".game-message");
+
+ this.score = 0;
+}
+
+HTMLActuator.prototype.actuate = function (grid, metadata) {
+ var self = this;
+
+ window.requestAnimationFrame(function () {
+ self.clearContainer(self.tileContainer);
+
+ grid.cells.forEach(function (column) {
+ column.forEach(function (cell) {
+ if (cell) {
+ self.addTile(cell);
+ }
+ });
+ });
+
+ self.updateScore(metadata.score);
+ self.updateBestScore(metadata.bestScore);
+
+ if (metadata.terminated) {
+ if (metadata.over) {
+ self.message(false); // You lose
+ } else if (metadata.won) {
+ self.message(true); // You win!
+ }
+ }
+
+ });
+};
+
+// Continues the game (both restart and keep playing)
+HTMLActuator.prototype.continueGame = function () {
+ this.clearMessage();
+};
+
+HTMLActuator.prototype.clearContainer = function (container) {
+ while (container.firstChild) {
+ container.removeChild(container.firstChild);
+ }
+};
+
+HTMLActuator.prototype.addTile = function (tile) {
+ var self = this;
+
+ var wrapper = document.createElement("div");
+ var inner = document.createElement("div");
+ var position = tile.previousPosition || { x: tile.x, y: tile.y };
+ var positionClass = this.positionClass(position);
+
+ // We can't use classlist because it somehow glitches when replacing classes
+ var classes = ["tile", "tile-" + tile.value, positionClass];
+
+ if (tile.value > 2048) classes.push("tile-super");
+
+ this.applyClasses(wrapper, classes);
+
+ inner.classList.add("tile-inner");
+ inner.textContent = tile.value;
+
+ if (tile.previousPosition) {
+ // Make sure that the tile gets rendered in the previous position first
+ window.requestAnimationFrame(function () {
+ classes[2] = self.positionClass({ x: tile.x, y: tile.y });
+ self.applyClasses(wrapper, classes); // Update the position
+ });
+ } else if (tile.mergedFrom) {
+ classes.push("tile-merged");
+ this.applyClasses(wrapper, classes);
+
+ // Render the tiles that merged
+ tile.mergedFrom.forEach(function (merged) {
+ self.addTile(merged);
+ });
+ } else {
+ classes.push("tile-new");
+ this.applyClasses(wrapper, classes);
+ }
+
+ // Add the inner part of the tile to the wrapper
+ wrapper.appendChild(inner);
+
+ // Put the tile on the board
+ this.tileContainer.appendChild(wrapper);
+};
+
+HTMLActuator.prototype.applyClasses = function (element, classes) {
+ element.setAttribute("class", classes.join(" "));
+};
+
+HTMLActuator.prototype.normalizePosition = function (position) {
+ return { x: position.x + 1, y: position.y + 1 };
+};
+
+HTMLActuator.prototype.positionClass = function (position) {
+ position = this.normalizePosition(position);
+ return "tile-position-" + position.x + "-" + position.y;
+};
+
+HTMLActuator.prototype.updateScore = function (score) {
+ this.clearContainer(this.scoreContainer);
+
+ var difference = score - this.score;
+ this.score = score;
+
+ this.scoreContainer.textContent = this.score;
+
+ if (difference > 0) {
+ var addition = document.createElement("div");
+ addition.classList.add("score-addition");
+ addition.textContent = "+" + difference;
+
+ this.scoreContainer.appendChild(addition);
+ }
+};
+
+HTMLActuator.prototype.updateBestScore = function (bestScore) {
+ this.bestContainer.textContent = bestScore;
+};
+
+HTMLActuator.prototype.message = function (won) {
+ var type = won ? "game-won" : "game-over";
+ var message = won ? "You win!" : "Game over!";
+
+ this.messageContainer.classList.add(type);
+ this.messageContainer.getElementsByTagName("p")[0].textContent = message;
+};
+
+HTMLActuator.prototype.clearMessage = function () {
+ // IE only takes one value to remove at a time.
+ this.messageContainer.classList.remove("game-won");
+ this.messageContainer.classList.remove("game-over");
+};
diff --git a/2048/js/keyboard_input_manager.js b/2048/js/keyboard_input_manager.js
new file mode 100644
index 0000000..ca01b3c
--- /dev/null
+++ b/2048/js/keyboard_input_manager.js
@@ -0,0 +1,144 @@
+function KeyboardInputManager() {
+ this.events = {};
+
+ if (window.navigator.msPointerEnabled) {
+ //Internet Explorer 10 style
+ this.eventTouchstart = "MSPointerDown";
+ this.eventTouchmove = "MSPointerMove";
+ this.eventTouchend = "MSPointerUp";
+ } else {
+ this.eventTouchstart = "touchstart";
+ this.eventTouchmove = "touchmove";
+ this.eventTouchend = "touchend";
+ }
+
+ this.listen();
+}
+
+KeyboardInputManager.prototype.on = function (event, callback) {
+ if (!this.events[event]) {
+ this.events[event] = [];
+ }
+ this.events[event].push(callback);
+};
+
+KeyboardInputManager.prototype.emit = function (event, data) {
+ var callbacks = this.events[event];
+ if (callbacks) {
+ callbacks.forEach(function (callback) {
+ callback(data);
+ });
+ }
+};
+
+KeyboardInputManager.prototype.listen = function () {
+ var self = this;
+
+ var map = {
+ 38: 0, // Up
+ 39: 1, // Right
+ 40: 2, // Down
+ 37: 3, // Left
+ 75: 0, // Vim up
+ 76: 1, // Vim right
+ 74: 2, // Vim down
+ 72: 3, // Vim left
+ 87: 0, // W
+ 68: 1, // D
+ 83: 2, // S
+ 65: 3 // A
+ };
+
+ // Respond to direction keys
+ document.addEventListener("keydown", function (event) {
+ var modifiers = event.altKey || event.ctrlKey || event.metaKey ||
+ event.shiftKey;
+ var mapped = map[event.which];
+
+ if (!modifiers) {
+ if (mapped !== undefined) {
+ event.preventDefault();
+ self.emit("move", mapped);
+ }
+ }
+
+ // R key restarts the game
+ if (!modifiers && event.which === 82) {
+ self.restart.call(self, event);
+ }
+ });
+
+ // Respond to button presses
+ this.bindButtonPress(".retry-button", this.restart);
+ this.bindButtonPress(".restart-button", this.restart);
+ this.bindButtonPress(".keep-playing-button", this.keepPlaying);
+
+ // Respond to swipe events
+ var touchStartClientX, touchStartClientY;
+ var gameContainer = document.getElementsByClassName("game-container")[0];
+
+ gameContainer.addEventListener(this.eventTouchstart, function (event) {
+ if ((!window.navigator.msPointerEnabled && event.touches.length > 1) ||
+ event.targetTouches.length > 1) {
+ return; // Ignore if touching with more than 1 finger
+ }
+
+ if (window.navigator.msPointerEnabled) {
+ touchStartClientX = event.pageX;
+ touchStartClientY = event.pageY;
+ } else {
+ touchStartClientX = event.touches[0].clientX;
+ touchStartClientY = event.touches[0].clientY;
+ }
+
+ event.preventDefault();
+ });
+
+ gameContainer.addEventListener(this.eventTouchmove, function (event) {
+ event.preventDefault();
+ });
+
+ gameContainer.addEventListener(this.eventTouchend, function (event) {
+ if ((!window.navigator.msPointerEnabled && event.touches.length > 0) ||
+ event.targetTouches.length > 0) {
+ return; // Ignore if still touching with one or more fingers
+ }
+
+ var touchEndClientX, touchEndClientY;
+
+ if (window.navigator.msPointerEnabled) {
+ touchEndClientX = event.pageX;
+ touchEndClientY = event.pageY;
+ } else {
+ touchEndClientX = event.changedTouches[0].clientX;
+ touchEndClientY = event.changedTouches[0].clientY;
+ }
+
+ var dx = touchEndClientX - touchStartClientX;
+ var absDx = Math.abs(dx);
+
+ var dy = touchEndClientY - touchStartClientY;
+ var absDy = Math.abs(dy);
+
+ if (Math.max(absDx, absDy) > 10) {
+ // (right : left) : (down : up)
+ self.emit("move", absDx > absDy ? (dx > 0 ? 1 : 3) : (dy > 0 ? 2 : 0));
+ }
+ });
+};
+
+KeyboardInputManager.prototype.restart = function (event) {
+ event.preventDefault();
+ this.emit("restart");
+};
+
+KeyboardInputManager.prototype.keepPlaying = function (event) {
+ event.preventDefault();
+ this.emit("keepPlaying");
+};
+
+KeyboardInputManager.prototype.bindButtonPress = function (selector, fn) {
+ var button = document.querySelector(selector);
+ button.addEventListener("click", fn.bind(this));
+ button.addEventListener(this.eventTouchend, fn.bind(this));
+};
diff --git a/2048/js/local_storage_manager.js b/2048/js/local_storage_manager.js
new file mode 100644
index 0000000..2ca9cc3
--- /dev/null
+++ b/2048/js/local_storage_manager.js
@@ -0,0 +1,63 @@
+window.fakeStorage = {
+ _data: {},
+
+ setItem: function (id, val) {
+ return this._data[id] = String(val);
+ },
+
+ getItem: function (id) {
+ return this._data.hasOwnProperty(id) ? this._data[id] : undefined;
+ },
+
+ removeItem: function (id) {
+ return delete this._data[id];
+ },
+
+ clear: function () {
+ return this._data = {};
+ }
+};
+
+function LocalStorageManager() {
+ this.bestScoreKey = "bestScore";
+ this.gameStateKey = "gameState";
+
+ var supported = this.localStorageSupported();
+ this.storage = supported ? window.localStorage : window.fakeStorage;
+}
+
+LocalStorageManager.prototype.localStorageSupported = function () {
+ var testKey = "test";
+
+ try {
+ var storage = window.localStorage;
+ storage.setItem(testKey, "1");
+ storage.removeItem(testKey);
+ return true;
+ } catch (error) {
+ return false;
+ }
+};
+
+// Best score getters/setters
+LocalStorageManager.prototype.getBestScore = function () {
+ return this.storage.getItem(this.bestScoreKey) || 0;
+};
+
+LocalStorageManager.prototype.setBestScore = function (score) {
+ this.storage.setItem(this.bestScoreKey, score);
+};
+
+// Game state getters/setters and clearing
+LocalStorageManager.prototype.getGameState = function () {
+ var stateJSON = this.storage.getItem(this.gameStateKey);
+ return stateJSON ? JSON.parse(stateJSON) : null;
+};
+
+LocalStorageManager.prototype.setGameState = function (gameState) {
+ this.storage.setItem(this.gameStateKey, JSON.stringify(gameState));
+};
+
+LocalStorageManager.prototype.clearGameState = function () {
+ this.storage.removeItem(this.gameStateKey);
+};
diff --git a/2048/js/tile.js b/2048/js/tile.js
new file mode 100644
index 0000000..92a670a
--- /dev/null
+++ b/2048/js/tile.js
@@ -0,0 +1,27 @@
+function Tile(position, value) {
+ this.x = position.x;
+ this.y = position.y;
+ this.value = value || 2;
+
+ this.previousPosition = null;
+ this.mergedFrom = null; // Tracks tiles that merged together
+}
+
+Tile.prototype.savePosition = function () {
+ this.previousPosition = { x: this.x, y: this.y };
+};
+
+Tile.prototype.updatePosition = function (position) {
+ this.x = position.x;
+ this.y = position.y;
+};
+
+Tile.prototype.serialize = function () {
+ return {
+ position: {
+ x: this.x,
+ y: this.y
+ },
+ value: this.value
+ };
+};
diff --git a/2048/meta/apple-touch-icon.png b/2048/meta/apple-touch-icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..3fd20f6092338ef464e440ec84a34c8850fd5d33
GIT binary patch
literal 5808
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