yethdev commited on
Commit
b11323b
·
verified ·
1 Parent(s): 2b62a4b

Upload folder using huggingface_hub

Browse files
Dockerfile ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM node:20-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY . .
6
+
7
+ ENV PORT=7860
8
+ EXPOSE 7860
9
+
10
+ CMD ["node", "--max-old-space-size=512", "server.js"]
LICENSE.txt ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 Gabriele Cirulli
4
+ Copyright (c) 2026 Yeth (yeth.dev)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ 1. The above copyright notice and this permission notice shall be included in
14
+ all copies or substantial portions of the Software.
15
+
16
+ 2. Credit must be given to Yeth as the original
17
+ author of the AI model, training code, and dataset in any derivative work,
18
+ publication, or distribution that uses them.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
26
+ THE SOFTWARE.
README.md CHANGED
@@ -1,10 +1,11 @@
1
- ---
2
- title: '2048'
3
- emoji: 🐠
4
- colorFrom: red
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
1
+ ---
2
+ title: "2048"
3
+ emoji: 🎮
4
+ colorFrom: yellow
5
+ colorTo: yellow
6
+ sdk: docker
7
+ pinned: false
8
+ license: mit
9
+ ---
10
+
11
+ Play 2048 powered by a trained N-Tuple Network AI agent.
favicon.ico ADDED
index.html ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html>
3
+
4
+ <head>
5
+ <meta charset="utf-8">
6
+ <title>2048</title>
7
+
8
+ <link href="style/main.css" rel="stylesheet" type="text/css">
9
+ <link rel="shortcut icon" href="favicon.ico">
10
+ <link rel="apple-touch-icon" href="meta/apple-touch-icon.png">
11
+ <link rel="apple-touch-startup-image" href="meta/apple-touch-startup-image-640x1096.png"
12
+ media="(device-width: 320px) and (device-height: 568px) and (-webkit-device-pixel-ratio: 2)">
13
+ <link rel="apple-touch-startup-image" href="meta/apple-touch-startup-image-640x920.png"
14
+ media="(device-width: 320px) and (device-height: 480px) and (-webkit-device-pixel-ratio: 2)">
15
+ <meta name="apple-mobile-web-app-capable" content="yes">
16
+ <meta name="apple-mobile-web-app-status-bar-style" content="black">
17
+
18
+ <meta name="HandheldFriendly" content="True">
19
+ <meta name="MobileOptimized" content="320">
20
+ <meta name="viewport"
21
+ content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0, maximum-scale=1, user-scalable=no, minimal-ui">
22
+ </head>
23
+
24
+ <body>
25
+ <div class="container">
26
+ <div class="heading">
27
+ <h1 class="title">2048</h1>
28
+ <div class="scores-container">
29
+ <div class="score-container">0</div>
30
+ <div class="best-container">0</div>
31
+ </div>
32
+ </div>
33
+
34
+ <div class="above-game">
35
+ <p class="game-intro">Join the numbers and get to the <strong>2048 tile!</strong></p>
36
+ <a class="restart-button">New Game</a>
37
+ </div>
38
+
39
+ <div class="game-container">
40
+ <div class="game-message">
41
+ <p></p>
42
+ <div class="lower">
43
+ <a class="keep-playing-button">Keep going</a>
44
+ <a class="retry-button">Try again</a>
45
+ </div>
46
+ </div>
47
+
48
+ <div class="grid-container">
49
+ <div class="grid-row">
50
+ <div class="grid-cell"></div>
51
+ <div class="grid-cell"></div>
52
+ <div class="grid-cell"></div>
53
+ <div class="grid-cell"></div>
54
+ </div>
55
+ <div class="grid-row">
56
+ <div class="grid-cell"></div>
57
+ <div class="grid-cell"></div>
58
+ <div class="grid-cell"></div>
59
+ <div class="grid-cell"></div>
60
+ </div>
61
+ <div class="grid-row">
62
+ <div class="grid-cell"></div>
63
+ <div class="grid-cell"></div>
64
+ <div class="grid-cell"></div>
65
+ <div class="grid-cell"></div>
66
+ </div>
67
+ <div class="grid-row">
68
+ <div class="grid-cell"></div>
69
+ <div class="grid-cell"></div>
70
+ <div class="grid-cell"></div>
71
+ <div class="grid-cell"></div>
72
+ </div>
73
+ </div>
74
+
75
+ <div class="tile-container">
76
+
77
+ </div>
78
+ </div>
79
+
80
+ <p class="game-explanation">
81
+ <strong class="important">How to play:</strong> Use your <strong>arrow keys</strong> to move the tiles. When
82
+ two tiles with the same number touch, they <strong>merge into one!</strong>
83
+ </p>
84
+ <hr>
85
+ <p>
86
+ <strong class="important">Note:</strong> This site is the official version of 2048. You can play it on your
87
+ phone via <a href="http://git.io/2048">http://git.io/2048.</a> All other apps or sites are derivatives or
88
+ fakes, and should be used with caution.
89
+ </p>
90
+ <hr>
91
+ <p>
92
+ Created by <a href="http://gabrielecirulli.com" target="_blank">Gabriele Cirulli.</a> Based on <a
93
+ href="https://itunes.apple.com/us/app/1024!/id823499224" target="_blank">1024 by Veewo Studio</a> and
94
+ conceptually similar to <a href="http://asherv.com/threes/" target="_blank">Threes by Asher Vollmer.</a>
95
+ </p>
96
+ </div>
97
+
98
+ <script src="js/bind_polyfill.js"></script>
99
+ <script src="js/classlist_polyfill.js"></script>
100
+ <script src="js/animframe_polyfill.js"></script>
101
+ <script src="js/keyboard_input_manager.js"></script>
102
+ <script src="js/html_actuator.js"></script>
103
+ <script src="js/grid.js"></script>
104
+ <script src="js/tile.js"></script>
105
+ <script src="js/local_storage_manager.js"></script>
106
+ <script src="js/game_manager.js"></script>
107
+ <script src="js/application.js"></script>
108
+ </body>
109
+
110
+ </html>
js/animframe_polyfill.js ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }());
js/application.js ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // boot the game and let the ai handle it
2
+ window.requestAnimationFrame(function () {
3
+ var gm = new GameManager(4, KeyboardInputManager, HTMLActuator, LocalStorageManager);
4
+
5
+ var aiDelay = 80;
6
+ var aiTimer = null;
7
+
8
+ function getCells() {
9
+ var cells = [];
10
+ for (var x = 0; x < 4; x++) {
11
+ cells[x] = [];
12
+ for (var y = 0; y < 4; y++) {
13
+ var t = gm.grid.cells[x][y];
14
+ cells[x][y] = t ? t.value : 0;
15
+ }
16
+ }
17
+ return cells;
18
+ }
19
+
20
+ function doMove() {
21
+ if (gm.over) {
22
+ // auto restart after a short pause
23
+ setTimeout(function () {
24
+ gm.storageManager.clearGameState();
25
+ gm.actuator.continueGame();
26
+ gm.setup();
27
+ scheduleNext();
28
+ }, 1500);
29
+ return;
30
+ }
31
+
32
+ // keep going past 2048
33
+ if (gm.won && typeof gm.keepPlaying !== "boolean") {
34
+ gm.keepPlaying();
35
+ } else if (gm.won && !gm.keepPlaying) {
36
+ gm.keepPlaying = true;
37
+ gm.actuator.continueGame();
38
+ }
39
+
40
+ var cells = getCells();
41
+
42
+ fetch("/api/move", {
43
+ method: "POST",
44
+ headers: { "Content-Type": "application/json" },
45
+ body: JSON.stringify({ cells: cells })
46
+ }).then(function (r) { return r.json(); })
47
+ .then(function (data) {
48
+ if (data.direction >= 0) {
49
+ gm.move(data.direction);
50
+ }
51
+ scheduleNext();
52
+ })
53
+ .catch(function () {
54
+ // server might be busy, retry
55
+ scheduleNext();
56
+ });
57
+ }
58
+
59
+ function scheduleNext() {
60
+ aiTimer = setTimeout(doMove, aiDelay);
61
+ }
62
+
63
+ // start after a tiny delay so the board renders first
64
+ setTimeout(function () {
65
+ scheduleNext();
66
+ }, 500);
67
+ });
js/bind_polyfill.js ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
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
+ };
js/classlist_polyfill.js ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ var classes = el.className.replace(/^\s+|\s+$/g, '').split(/\s+/);
15
+ for (var i = 0; i < classes.length; i++) {
16
+ push.call(this, classes[i]);
17
+ }
18
+ }
19
+
20
+ DOMTokenList.prototype = {
21
+ add: function (token) {
22
+ if (this.contains(token)) return;
23
+ push.call(this, token);
24
+ this.el.className = this.toString();
25
+ },
26
+ contains: function (token) {
27
+ return this.el.className.indexOf(token) != -1;
28
+ },
29
+ item: function (index) {
30
+ return this[index] || null;
31
+ },
32
+ remove: function (token) {
33
+ if (!this.contains(token)) return;
34
+ for (var i = 0; i < this.length; i++) {
35
+ if (this[i] == token) break;
36
+ }
37
+ splice.call(this, i, 1);
38
+ this.el.className = this.toString();
39
+ },
40
+ toString: function () {
41
+ return join.call(this, ' ');
42
+ },
43
+ toggle: function (token) {
44
+ if (!this.contains(token)) {
45
+ this.add(token);
46
+ } else {
47
+ this.remove(token);
48
+ }
49
+
50
+ return this.contains(token);
51
+ }
52
+ };
53
+
54
+ window.DOMTokenList = DOMTokenList;
55
+
56
+ function defineElementGetter(obj, prop, getter) {
57
+ if (Object.defineProperty) {
58
+ Object.defineProperty(obj, prop, {
59
+ get: getter
60
+ });
61
+ } else {
62
+ obj.__defineGetter__(prop, getter);
63
+ }
64
+ }
65
+
66
+ defineElementGetter(HTMLElement.prototype, 'classList', function () {
67
+ return new DOMTokenList(this);
68
+ });
69
+ })();
js/game_manager.js ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function GameManager(size, InputManager, Actuator, StorageManager) {
2
+ this.size = size;
3
+ this.inputManager = new InputManager;
4
+ this.storageManager = new StorageManager;
5
+ this.actuator = new Actuator;
6
+
7
+ this.startTiles = 2;
8
+ this.maxRNG = false;
9
+ this.ntupleAI = null;
10
+
11
+ this.inputManager.on("move", this.move.bind(this));
12
+ this.inputManager.on("restart", this.restart.bind(this));
13
+ this.inputManager.on("keepPlaying", this.keepPlaying.bind(this));
14
+
15
+ this.setup();
16
+ }
17
+
18
+
19
+ GameManager.prototype.restart = function () {
20
+ this.storageManager.clearGameState();
21
+ this.actuator.continueGame();
22
+ this.setup();
23
+ };
24
+
25
+
26
+ GameManager.prototype.keepPlaying = function () {
27
+ this.keepPlaying = true;
28
+ this.actuator.continueGame();
29
+ };
30
+
31
+
32
+ GameManager.prototype.isGameTerminated = function () {
33
+ return this.over || (this.won && !this.keepPlaying);
34
+ };
35
+
36
+
37
+ GameManager.prototype.setup = function () {
38
+ var previousState = this.storageManager.getGameState();
39
+
40
+
41
+ if (previousState) {
42
+ this.grid = new Grid(previousState.grid.size,
43
+ previousState.grid.cells);
44
+ this.score = previousState.score;
45
+ this.over = previousState.over;
46
+ this.won = previousState.won;
47
+ this.keepPlaying = previousState.keepPlaying;
48
+ } else {
49
+ this.grid = new Grid(this.size);
50
+ this.score = 0;
51
+ this.over = false;
52
+ this.won = false;
53
+ this.keepPlaying = false;
54
+
55
+
56
+ this.addStartTiles();
57
+ }
58
+
59
+
60
+ this.actuate();
61
+ };
62
+
63
+
64
+ GameManager.prototype.addStartTiles = function () {
65
+ for (var i = 0; i < this.startTiles; i++) {
66
+ this.addRandomTile();
67
+ }
68
+ };
69
+
70
+
71
+ GameManager.prototype.addRandomTile = function () {
72
+ if (this.grid.cellsAvailable()) {
73
+
74
+ if (this.pendingTile) {
75
+ // server told us exactly where to put it
76
+ var pt = this.pendingTile;
77
+ this.pendingTile = null;
78
+ var tile = new Tile({ x: pt.x, y: pt.y }, pt.value);
79
+ this.grid.insertTile(tile);
80
+ } else if (this.maxRNG && this.ntupleAI) {
81
+
82
+ var cells = [];
83
+ for (var x = 0; x < this.size; x++) {
84
+ cells[x] = [];
85
+ for (var y = 0; y < this.size; y++) {
86
+ var t = this.grid.cells[x][y];
87
+ cells[x][y] = t ? t.value : 0;
88
+ }
89
+ }
90
+
91
+ var empty = this.grid.availableCells();
92
+ var bestScore = -Infinity;
93
+ var bestCell = empty[0];
94
+ var bestValue = 2;
95
+
96
+ for (var i = 0; i < empty.length; i++) {
97
+ var c = empty[i];
98
+
99
+ cells[c.x][c.y] = 2;
100
+ var s2 = this.ntupleAI.evaluate(cells);
101
+ if (s2 > bestScore) { bestScore = s2; bestCell = c; bestValue = 2; }
102
+
103
+ cells[c.x][c.y] = 4;
104
+ var s4 = this.ntupleAI.evaluate(cells);
105
+ if (s4 > bestScore) { bestScore = s4; bestCell = c; bestValue = 4; }
106
+ cells[c.x][c.y] = 0;
107
+ }
108
+
109
+ var tile = new Tile(bestCell, bestValue);
110
+ this.grid.insertTile(tile);
111
+ } else {
112
+ var value = Math.random() < 0.9 ? 2 : 4;
113
+ var tile = new Tile(this.grid.randomAvailableCell(), value);
114
+ this.grid.insertTile(tile);
115
+ }
116
+ }
117
+ };
118
+
119
+
120
+ GameManager.prototype.actuate = function () {
121
+ if (this.storageManager.getBestScore() < this.score) {
122
+ this.storageManager.setBestScore(this.score);
123
+ }
124
+
125
+
126
+ if (this.over) {
127
+ this.storageManager.clearGameState();
128
+ } else {
129
+ this.storageManager.setGameState(this.serialize());
130
+ }
131
+
132
+ this.actuator.actuate(this.grid, {
133
+ score: this.score,
134
+ over: this.over,
135
+ won: this.won,
136
+ bestScore: this.storageManager.getBestScore(),
137
+ terminated: this.isGameTerminated()
138
+ });
139
+
140
+ };
141
+
142
+
143
+ GameManager.prototype.serialize = function () {
144
+ return {
145
+ grid: this.grid.serialize(),
146
+ score: this.score,
147
+ over: this.over,
148
+ won: this.won,
149
+ keepPlaying: this.keepPlaying
150
+ };
151
+ };
152
+
153
+
154
+ GameManager.prototype.prepareTiles = function () {
155
+ this.grid.eachCell(function (x, y, tile) {
156
+ if (tile) {
157
+ tile.mergedFrom = null;
158
+ tile.savePosition();
159
+ }
160
+ });
161
+ };
162
+
163
+
164
+ GameManager.prototype.moveTile = function (tile, cell) {
165
+ this.grid.cells[tile.x][tile.y] = null;
166
+ this.grid.cells[cell.x][cell.y] = tile;
167
+ tile.updatePosition(cell);
168
+ };
169
+
170
+
171
+ GameManager.prototype.move = function (direction) {
172
+
173
+ var self = this;
174
+
175
+ if (this.isGameTerminated()) return;
176
+
177
+ var cell, tile;
178
+
179
+ var vector = this.getVector(direction);
180
+ var traversals = this.buildTraversals(vector);
181
+ var moved = false;
182
+
183
+
184
+ this.prepareTiles();
185
+
186
+
187
+ traversals.x.forEach(function (x) {
188
+ traversals.y.forEach(function (y) {
189
+ cell = { x: x, y: y };
190
+ tile = self.grid.cellContent(cell);
191
+
192
+ if (tile) {
193
+ var positions = self.findFarthestPosition(cell, vector);
194
+ var next = self.grid.cellContent(positions.next);
195
+
196
+
197
+ if (next && next.value === tile.value && !next.mergedFrom) {
198
+ var merged = new Tile(positions.next, tile.value * 2);
199
+ merged.mergedFrom = [tile, next];
200
+
201
+ self.grid.insertTile(merged);
202
+ self.grid.removeTile(tile);
203
+
204
+
205
+ tile.updatePosition(positions.next);
206
+
207
+
208
+ self.score += merged.value;
209
+
210
+
211
+ if (merged.value === 2048) self.won = true;
212
+ } else {
213
+ self.moveTile(tile, positions.farthest);
214
+ }
215
+
216
+ if (!self.positionsEqual(cell, tile)) {
217
+ moved = true;
218
+ }
219
+ }
220
+ });
221
+ });
222
+
223
+ if (moved) {
224
+ this.addRandomTile();
225
+
226
+ if (!this.movesAvailable()) {
227
+ this.over = true;
228
+ }
229
+
230
+ this.actuate();
231
+ }
232
+ };
233
+
234
+
235
+ GameManager.prototype.getVector = function (direction) {
236
+
237
+ var map = {
238
+ 0: { x: 0, y: -1 },
239
+ 1: { x: 1, y: 0 },
240
+ 2: { x: 0, y: 1 },
241
+ 3: { x: -1, y: 0 }
242
+ };
243
+
244
+ return map[direction];
245
+ };
246
+
247
+
248
+ GameManager.prototype.buildTraversals = function (vector) {
249
+ var traversals = { x: [], y: [] };
250
+
251
+ for (var pos = 0; pos < this.size; pos++) {
252
+ traversals.x.push(pos);
253
+ traversals.y.push(pos);
254
+ }
255
+
256
+
257
+ if (vector.x === 1) traversals.x = traversals.x.reverse();
258
+ if (vector.y === 1) traversals.y = traversals.y.reverse();
259
+
260
+ return traversals;
261
+ };
262
+
263
+ GameManager.prototype.findFarthestPosition = function (cell, vector) {
264
+ var previous;
265
+
266
+
267
+ do {
268
+ previous = cell;
269
+ cell = { x: previous.x + vector.x, y: previous.y + vector.y };
270
+ } while (this.grid.withinBounds(cell) &&
271
+ this.grid.cellAvailable(cell));
272
+
273
+ return {
274
+ farthest: previous,
275
+ next: cell
276
+ };
277
+ };
278
+
279
+ GameManager.prototype.movesAvailable = function () {
280
+ return this.grid.cellsAvailable() || this.tileMatchesAvailable();
281
+ };
282
+
283
+
284
+ GameManager.prototype.tileMatchesAvailable = function () {
285
+ var self = this;
286
+
287
+ var tile;
288
+
289
+ for (var x = 0; x < this.size; x++) {
290
+ for (var y = 0; y < this.size; y++) {
291
+ tile = this.grid.cellContent({ x: x, y: y });
292
+
293
+ if (tile) {
294
+ for (var direction = 0; direction < 4; direction++) {
295
+ var vector = self.getVector(direction);
296
+ var cell = { x: x + vector.x, y: y + vector.y };
297
+
298
+ var other = self.grid.cellContent(cell);
299
+
300
+ if (other && other.value === tile.value) {
301
+ return true;
302
+ }
303
+ }
304
+ }
305
+ }
306
+ }
307
+
308
+ return false;
309
+ };
310
+
311
+ GameManager.prototype.positionsEqual = function (first, second) {
312
+ return first.x === second.x && first.y === second.y;
313
+ };
js/grid.js ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function Grid(size, previousState) {
2
+ this.size = size;
3
+ this.cells = previousState ? this.fromState(previousState) : this.empty();
4
+ }
5
+
6
+
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
+
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
+
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
+
67
+ Grid.prototype.cellsAvailable = function () {
68
+ return !!this.availableCells().length;
69
+ };
70
+
71
+
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
+
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
+ };
js/html_actuator.js ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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);
30
+ } else if (metadata.won) {
31
+ self.message(true);
32
+ }
33
+ }
34
+
35
+ });
36
+ };
37
+
38
+
39
+ HTMLActuator.prototype.continueGame = function () {
40
+ this.clearMessage();
41
+ };
42
+
43
+ HTMLActuator.prototype.clearContainer = function (container) {
44
+ while (container.firstChild) {
45
+ container.removeChild(container.firstChild);
46
+ }
47
+ };
48
+
49
+ HTMLActuator.prototype.addTile = function (tile) {
50
+ var self = this;
51
+
52
+ var wrapper = document.createElement("div");
53
+ var inner = document.createElement("div");
54
+ var position = tile.previousPosition || { x: tile.x, y: tile.y };
55
+ var positionClass = this.positionClass(position);
56
+
57
+
58
+ var classes = ["tile", "tile-" + tile.value, positionClass];
59
+
60
+ if (tile.value > 2048) classes.push("tile-super");
61
+
62
+ this.applyClasses(wrapper, classes);
63
+
64
+ inner.classList.add("tile-inner");
65
+ inner.textContent = tile.value;
66
+
67
+ if (tile.previousPosition) {
68
+
69
+ window.requestAnimationFrame(function () {
70
+ classes[2] = self.positionClass({ x: tile.x, y: tile.y });
71
+ self.applyClasses(wrapper, classes);
72
+ });
73
+ } else if (tile.mergedFrom) {
74
+ classes.push("tile-merged");
75
+ this.applyClasses(wrapper, classes);
76
+
77
+
78
+ tile.mergedFrom.forEach(function (merged) {
79
+ self.addTile(merged);
80
+ });
81
+ } else {
82
+ classes.push("tile-new");
83
+ this.applyClasses(wrapper, classes);
84
+ }
85
+
86
+
87
+ wrapper.appendChild(inner);
88
+
89
+
90
+ this.tileContainer.appendChild(wrapper);
91
+ };
92
+
93
+ HTMLActuator.prototype.applyClasses = function (element, classes) {
94
+ element.setAttribute("class", classes.join(" "));
95
+ };
96
+
97
+ HTMLActuator.prototype.normalizePosition = function (position) {
98
+ return { x: position.x + 1, y: position.y + 1 };
99
+ };
100
+
101
+ HTMLActuator.prototype.positionClass = function (position) {
102
+ position = this.normalizePosition(position);
103
+ return "tile-position-" + position.x + "-" + position.y;
104
+ };
105
+
106
+ HTMLActuator.prototype.updateScore = function (score) {
107
+ this.clearContainer(this.scoreContainer);
108
+
109
+ var difference = score - this.score;
110
+ this.score = score;
111
+
112
+ this.scoreContainer.textContent = this.score;
113
+
114
+ if (difference > 0) {
115
+ var addition = document.createElement("div");
116
+ addition.classList.add("score-addition");
117
+ addition.textContent = "+" + difference;
118
+
119
+ this.scoreContainer.appendChild(addition);
120
+ }
121
+ };
122
+
123
+ HTMLActuator.prototype.updateBestScore = function (bestScore) {
124
+ this.bestContainer.textContent = bestScore;
125
+ };
126
+
127
+ HTMLActuator.prototype.message = function (won) {
128
+ var type = won ? "game-won" : "game-over";
129
+ var message = won ? "You win!" : "Game over!";
130
+
131
+ this.messageContainer.classList.add(type);
132
+ this.messageContainer.getElementsByTagName("p")[0].textContent = message;
133
+ };
134
+
135
+ HTMLActuator.prototype.clearMessage = function () {
136
+
137
+ this.messageContainer.classList.remove("game-won");
138
+ this.messageContainer.classList.remove("game-over");
139
+ };
js/keyboard_input_manager.js ADDED
@@ -0,0 +1,144 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ function KeyboardInputManager() {
2
+ this.events = {};
3
+
4
+ if (window.navigator.msPointerEnabled) {
5
+
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,
39
+ 39: 1,
40
+ 40: 2,
41
+ 37: 3,
42
+ 75: 0,
43
+ 76: 1,
44
+ 74: 2,
45
+ 72: 3,
46
+ 87: 0,
47
+ 68: 1,
48
+ 83: 2,
49
+ 65: 3
50
+ };
51
+
52
+
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
+
66
+ if (!modifiers && event.which === 82) {
67
+ self.restart.call(self, event);
68
+ }
69
+ });
70
+
71
+
72
+ this.bindButtonPress(".retry-button", this.restart);
73
+ this.bindButtonPress(".restart-button", this.restart);
74
+ this.bindButtonPress(".keep-playing-button", this.keepPlaying);
75
+
76
+
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;
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;
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
+
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
+ };
js/local_storage_manager.js ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
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
+
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
+ };
js/tile.js ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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;
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
+ };
meta/apple-touch-icon.png ADDED
meta/apple-touch-startup-image-640x1096.png ADDED
meta/apple-touch-startup-image-640x920.png ADDED
model_meta.json ADDED
@@ -0,0 +1 @@
 
 
1
+ {"gamesPlayed":1200000,"patterns":8,"numValues":15,"tupleSize":6,"maxTile":16384,"learningRate":0.0005,"timestamp":1775703954243}
model_weights.bin ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:10dc981c51c646d271fdffba796557e79c7d8c892456ccf3140ec3e4b85866aa
3
+ size 364500000
server.js ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ var http = require('http');
2
+ var fs = require('fs');
3
+ var path = require('path');
4
+
5
+ // model constants
6
+ var NUM_VALUES = 15;
7
+ var TABLE_SIZE = Math.pow(NUM_VALUES, 6);
8
+ var NUM_PATTERNS = 8;
9
+
10
+ var PATTERNS = [
11
+ [[0, 0], [1, 0], [2, 0], [3, 0], [0, 1], [1, 1]],
12
+ [[0, 0], [1, 0], [0, 1], [1, 1], [0, 2], [1, 2]],
13
+ [[0, 0], [1, 0], [2, 0], [0, 1], [1, 1], [2, 1]],
14
+ [[0, 0], [1, 0], [0, 1], [1, 1], [2, 1], [3, 1]],
15
+ [[0, 0], [1, 0], [2, 0], [3, 0], [2, 1], [3, 1]],
16
+ [[0, 0], [1, 0], [2, 0], [1, 1], [2, 1], [3, 1]],
17
+ [[0, 0], [1, 0], [0, 1], [1, 1], [2, 1], [2, 2]],
18
+ [[0, 0], [1, 0], [2, 0], [0, 1], [1, 1], [1, 2]],
19
+ ];
20
+
21
+ var SYMMETRY_FNS = [
22
+ function (x, y) { return [x, y] },
23
+ function (x, y) { return [3 - x, y] },
24
+ function (x, y) { return [x, 3 - y] },
25
+ function (x, y) { return [3 - x, 3 - y] },
26
+ function (x, y) { return [y, x] },
27
+ function (x, y) { return [3 - y, x] },
28
+ function (x, y) { return [y, 3 - x] },
29
+ function (x, y) { return [3 - y, 3 - x] },
30
+ ];
31
+
32
+ // precompute tuple lookups
33
+ var NUM_TUPLES = NUM_PATTERNS * 8;
34
+ var tupleCoords = new Int32Array(NUM_TUPLES * 6);
35
+ var tupleTable = new Int32Array(NUM_TUPLES);
36
+
37
+ var ti = 0;
38
+ for (var p = 0; p < NUM_PATTERNS; p++) {
39
+ for (var s = 0; s < 8; s++) {
40
+ tupleTable[ti] = p;
41
+ for (var i = 0; i < 6; i++) {
42
+ var c = SYMMETRY_FNS[s](PATTERNS[p][i][0], PATTERNS[p][i][1]);
43
+ tupleCoords[ti * 6 + i] = c[0] * 4 + c[1];
44
+ }
45
+ ti++;
46
+ }
47
+ }
48
+
49
+ // tile index mapping
50
+ var TILE_TO_IDX = new Uint8Array(65536);
51
+ TILE_TO_IDX[0] = 0;
52
+ for (var i = 1; i <= 16; i++) {
53
+ TILE_TO_IDX[Math.min(1 << i, 65535)] = Math.min(i, NUM_VALUES - 1);
54
+ }
55
+
56
+ // allocate + load weights
57
+ console.log('Loading model weights...');
58
+ var weights = [];
59
+ for (var i = 0; i < NUM_PATTERNS; i++) {
60
+ weights.push(new Float32Array(TABLE_SIZE));
61
+ }
62
+
63
+ // check local dir first, then parent (works both standalone and in-tree)
64
+ var WEIGHTS_FILE = fs.existsSync(path.join(__dirname, 'model_weights.bin'))
65
+ ? path.join(__dirname, 'model_weights.bin')
66
+ : path.join(__dirname, '..', 'model_weights.bin');
67
+ var META_FILE = fs.existsSync(path.join(__dirname, 'model_meta.json'))
68
+ ? path.join(__dirname, 'model_meta.json')
69
+ : path.join(__dirname, '..', 'model_meta.json');
70
+
71
+ if (!fs.existsSync(WEIGHTS_FILE)) {
72
+ console.error('model_weights.bin not found. Train the model first.');
73
+ process.exit(1);
74
+ }
75
+
76
+ var meta = JSON.parse(fs.readFileSync(META_FILE, 'utf8'));
77
+ var buf = fs.readFileSync(WEIGHTS_FILE);
78
+ var off = 0;
79
+ for (var i = 0; i < NUM_PATTERNS; i++) {
80
+ var src = new Float32Array(buf.buffer, buf.byteOffset + off, TABLE_SIZE);
81
+ weights[i].set(src);
82
+ off += TABLE_SIZE * 4;
83
+ }
84
+ console.log('Model loaded (' + meta.gamesPlayed.toLocaleString() + ' games, max tile ' + meta.maxTile + ')');
85
+
86
+
87
+ function evaluate(board) {
88
+ var score = 0;
89
+ var ci = 0;
90
+ for (var t = 0; t < NUM_TUPLES; t++) {
91
+ var idx = board[tupleCoords[ci]];
92
+ idx = idx * NUM_VALUES + board[tupleCoords[ci + 1]];
93
+ idx = idx * NUM_VALUES + board[tupleCoords[ci + 2]];
94
+ idx = idx * NUM_VALUES + board[tupleCoords[ci + 3]];
95
+ idx = idx * NUM_VALUES + board[tupleCoords[ci + 4]];
96
+ idx = idx * NUM_VALUES + board[tupleCoords[ci + 5]];
97
+ score += weights[tupleTable[t]][idx];
98
+ ci += 6;
99
+ }
100
+ return score;
101
+ }
102
+
103
+ var _merged = new Uint8Array(16);
104
+ var _tempBoards = [new Uint8Array(16), new Uint8Array(16),
105
+ new Uint8Array(16), new Uint8Array(16)];
106
+
107
+ var VECTORS_X = [0, 1, 0, -1];
108
+ var VECTORS_Y = [-1, 0, 1, 0];
109
+
110
+ function simulateMove(board, dir, out) {
111
+ out.set(board);
112
+ _merged.fill(0);
113
+ var reward = 0, moved = false;
114
+ var vx = VECTORS_X[dir], vy = VECTORS_Y[dir];
115
+ var tx = vx === 1 ? [3, 2, 1, 0] : [0, 1, 2, 3];
116
+ var ty = vy === 1 ? [3, 2, 1, 0] : [0, 1, 2, 3];
117
+
118
+ for (var i = 0; i < 4; i++) {
119
+ for (var j = 0; j < 4; j++) {
120
+ var cx = tx[i], cy = ty[j];
121
+ var ci = cx * 4 + cy;
122
+ var val = out[ci];
123
+ if (val === 0) continue;
124
+ var px = cx, py = cy;
125
+ var nx = cx + vx, ny = cy + vy;
126
+ while (nx >= 0 && nx < 4 && ny >= 0 && ny < 4 && out[nx * 4 + ny] === 0) {
127
+ px = nx; py = ny; nx += vx; ny += vy;
128
+ }
129
+ var ni = nx * 4 + ny, pi = px * 4 + py;
130
+ if (nx >= 0 && nx < 4 && ny >= 0 && ny < 4 && out[ni] === val && !_merged[ni]) {
131
+ out[ci] = 0;
132
+ var nv = val + 1;
133
+ out[ni] = nv;
134
+ _merged[ni] = 1;
135
+ reward += (nv < NUM_VALUES) ? (1 << nv) : (1 << (NUM_VALUES - 1));
136
+ moved = true;
137
+ } else if (pi !== ci) {
138
+ out[ci] = 0;
139
+ out[pi] = val;
140
+ moved = true;
141
+ }
142
+ }
143
+ }
144
+ return { reward: reward, moved: moved };
145
+ }
146
+
147
+ function getBestMove(board) {
148
+ var bestScore = -Infinity;
149
+ var bestDir = -1;
150
+ for (var d = 0; d < 4; d++) {
151
+ var r = simulateMove(board, d, _tempBoards[d]);
152
+ if (!r.moved) continue;
153
+ var sc = r.reward + evaluate(_tempBoards[d]);
154
+ if (sc > bestScore) { bestScore = sc; bestDir = d; }
155
+ }
156
+ return bestDir;
157
+ }
158
+
159
+ // convert browser cell format to internal indices
160
+ function boardFromCells(cells) {
161
+ var board = new Uint8Array(16);
162
+ for (var x = 0; x < 4; x++)
163
+ for (var y = 0; y < 4; y++) {
164
+ var v = cells[x][y];
165
+ board[x * 4 + y] = v === 0 ? 0 : (TILE_TO_IDX[v] || 0);
166
+ }
167
+ return board;
168
+ }
169
+
170
+
171
+ // static file server
172
+ var MIME = {
173
+ '.html': 'text/html', '.css': 'text/css',
174
+ '.js': 'application/javascript', '.json': 'application/json',
175
+ '.ico': 'image/x-icon', '.png': 'image/png',
176
+ '.jpg': 'image/jpeg', '.woff': 'font/woff',
177
+ '.woff2': 'font/woff2', '.ttf': 'font/ttf',
178
+ '.svg': 'image/svg+xml',
179
+ };
180
+
181
+ var RUNNER_DIR = path.resolve(__dirname);
182
+ var PROJECT_DIR = path.resolve(__dirname, '..');
183
+
184
+ // HF Spaces uses port 7860
185
+ var PORT = parseInt(process.env.PORT, 10) || 3000;
186
+
187
+ function serveStatic(reqPath, res) {
188
+ var safePath = reqPath === '/' ? '/index.html' : reqPath;
189
+ safePath = decodeURIComponent(safePath);
190
+
191
+ // try runner dir first (overrides), then project root
192
+ var runnerFile = path.resolve(RUNNER_DIR, '.' + safePath);
193
+ var projectFile = path.resolve(PROJECT_DIR, '.' + safePath);
194
+
195
+ var filePath;
196
+ if (runnerFile.startsWith(RUNNER_DIR) && fs.existsSync(runnerFile) &&
197
+ fs.statSync(runnerFile).isFile()) {
198
+ filePath = runnerFile;
199
+ } else if (projectFile.startsWith(PROJECT_DIR)) {
200
+ filePath = projectFile;
201
+ } else {
202
+ res.writeHead(403); res.end('Forbidden');
203
+ return;
204
+ }
205
+
206
+ fs.readFile(filePath, function (err, data) {
207
+ if (err) {
208
+ res.writeHead(404); res.end('Not found');
209
+ return;
210
+ }
211
+ var ext = path.extname(filePath).toLowerCase();
212
+ res.writeHead(200, { 'Content-Type': MIME[ext] || 'application/octet-stream' });
213
+ res.end(data);
214
+ });
215
+ }
216
+
217
+ function readBody(req) {
218
+ return new Promise(function (resolve, reject) {
219
+ var chunks = [];
220
+ var size = 0;
221
+ req.on('data', function (chunk) {
222
+ size += chunk.length;
223
+ if (size > 102400) { reject(new Error('too big')); req.destroy(); return; }
224
+ chunks.push(chunk);
225
+ });
226
+ req.on('end', function () { resolve(Buffer.concat(chunks).toString()); });
227
+ req.on('error', reject);
228
+ });
229
+ }
230
+
231
+
232
+ var server = http.createServer(function (req, res) {
233
+ var url = new (require('url').URL)(req.url, 'http://localhost');
234
+ var pathname = url.pathname;
235
+
236
+ // single API endpoint - get best move for a board state
237
+ if (pathname === '/api/move' && req.method === 'POST') {
238
+ readBody(req).then(function (raw) {
239
+ var body = JSON.parse(raw);
240
+ var board = boardFromCells(body.cells);
241
+ var dir = getBestMove(board);
242
+
243
+ res.writeHead(200, {
244
+ 'Content-Type': 'application/json',
245
+ 'Access-Control-Allow-Origin': '*'
246
+ });
247
+ res.end(JSON.stringify({ direction: dir }));
248
+ }).catch(function (e) {
249
+ res.writeHead(400, { 'Content-Type': 'application/json' });
250
+ res.end(JSON.stringify({ error: e.message }));
251
+ });
252
+ return;
253
+ }
254
+
255
+ if (req.method === 'OPTIONS') {
256
+ res.writeHead(204, {
257
+ 'Access-Control-Allow-Origin': '*',
258
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
259
+ 'Access-Control-Allow-Headers': 'Content-Type'
260
+ });
261
+ res.end();
262
+ return;
263
+ }
264
+
265
+ serveStatic(pathname, res);
266
+ });
267
+
268
+ server.listen(PORT, function () {
269
+ console.log('2048 running at http://localhost:' + PORT);
270
+ });
style/main.css ADDED
@@ -0,0 +1,819 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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: 80px;
20
+ font-weight: bold;
21
+ margin: 0;
22
+ display: block;
23
+ float: left; }
24
+
25
+ @-webkit-keyframes move-up {
26
+ 0% {
27
+ top: 25px;
28
+ opacity: 1; }
29
+
30
+ 100% {
31
+ top: -50px;
32
+ opacity: 0; } }
33
+ @-moz-keyframes move-up {
34
+ 0% {
35
+ top: 25px;
36
+ opacity: 1; }
37
+
38
+ 100% {
39
+ top: -50px;
40
+ opacity: 0; } }
41
+ @keyframes move-up {
42
+ 0% {
43
+ top: 25px;
44
+ opacity: 1; }
45
+
46
+ 100% {
47
+ top: -50px;
48
+ opacity: 0; } }
49
+ .scores-container {
50
+ float: 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: "Score"; }
94
+
95
+ .best-container:after {
96
+ content: "Best"; }
97
+
98
+ p {
99
+ margin-top: 0;
100
+ margin-bottom: 10px;
101
+ line-height: 1.65; }
102
+
103
+ a {
104
+ color: #776e65;
105
+ font-weight: bold;
106
+ text-decoration: underline;
107
+ cursor: pointer; }
108
+
109
+ strong.important {
110
+ text-transform: uppercase; }
111
+
112
+ hr {
113
+ border: none;
114
+ border-bottom: 1px solid #d8d4d0;
115
+ margin-top: 20px;
116
+ margin-bottom: 30px; }
117
+
118
+ .container {
119
+ width: 500px;
120
+ margin: 0 auto; }
121
+
122
+ @-webkit-keyframes fade-in {
123
+ 0% {
124
+ opacity: 0; }
125
+
126
+ 100% {
127
+ opacity: 1; } }
128
+ @-moz-keyframes fade-in {
129
+ 0% {
130
+ opacity: 0; }
131
+
132
+ 100% {
133
+ opacity: 1; } }
134
+ @keyframes fade-in {
135
+ 0% {
136
+ opacity: 0; }
137
+
138
+ 100% {
139
+ opacity: 1; } }
140
+ .game-container {
141
+ margin-top: 40px;
142
+ position: relative;
143
+ padding: 15px;
144
+ cursor: default;
145
+ -webkit-touch-callout: none;
146
+ -ms-touch-callout: none;
147
+ -webkit-user-select: none;
148
+ -moz-user-select: none;
149
+ -ms-user-select: none;
150
+ -ms-touch-action: none;
151
+ touch-action: none;
152
+ background: #bbada0;
153
+ border-radius: 6px;
154
+ width: 500px;
155
+ height: 500px;
156
+ -webkit-box-sizing: border-box;
157
+ -moz-box-sizing: border-box;
158
+ box-sizing: border-box; }
159
+ .game-container .game-message {
160
+ display: none;
161
+ position: absolute;
162
+ top: 0;
163
+ right: 0;
164
+ bottom: 0;
165
+ left: 0;
166
+ background: rgba(238, 228, 218, 0.5);
167
+ z-index: 100;
168
+ text-align: center;
169
+ -webkit-animation: fade-in 800ms ease 1200ms;
170
+ -moz-animation: fade-in 800ms ease 1200ms;
171
+ animation: fade-in 800ms ease 1200ms;
172
+ -webkit-animation-fill-mode: both;
173
+ -moz-animation-fill-mode: both;
174
+ animation-fill-mode: both; }
175
+ .game-container .game-message p {
176
+ font-size: 60px;
177
+ font-weight: bold;
178
+ height: 60px;
179
+ line-height: 60px;
180
+ margin-top: 222px; }
181
+ .game-container .game-message .lower {
182
+ display: block;
183
+ margin-top: 59px; }
184
+ .game-container .game-message a {
185
+ display: inline-block;
186
+ background: #8f7a66;
187
+ border-radius: 3px;
188
+ padding: 0 20px;
189
+ text-decoration: none;
190
+ color: #f9f6f2;
191
+ height: 40px;
192
+ line-height: 42px;
193
+ margin-left: 9px; }
194
+ .game-container .game-message a.keep-playing-button {
195
+ display: none; }
196
+ .game-container .game-message.game-won {
197
+ background: rgba(237, 194, 46, 0.5);
198
+ color: #f9f6f2; }
199
+ .game-container .game-message.game-won a.keep-playing-button {
200
+ display: inline-block; }
201
+ .game-container .game-message.game-won, .game-container .game-message.game-over {
202
+ display: block; }
203
+
204
+ .grid-container {
205
+ position: absolute;
206
+ z-index: 1; }
207
+
208
+ .grid-row {
209
+ margin-bottom: 15px; }
210
+ .grid-row:last-child {
211
+ margin-bottom: 0; }
212
+ .grid-row:after {
213
+ content: "";
214
+ display: block;
215
+ clear: both; }
216
+
217
+ .grid-cell {
218
+ width: 106.25px;
219
+ height: 106.25px;
220
+ margin-right: 15px;
221
+ float: left;
222
+ border-radius: 3px;
223
+ background: rgba(238, 228, 218, 0.35); }
224
+ .grid-cell:last-child {
225
+ margin-right: 0; }
226
+
227
+ .tile-container {
228
+ position: absolute;
229
+ z-index: 2; }
230
+
231
+ .tile, .tile .tile-inner {
232
+ width: 107px;
233
+ height: 107px;
234
+ line-height: 107px; }
235
+ .tile.tile-position-1-1 {
236
+ -webkit-transform: translate(0px, 0px);
237
+ -moz-transform: translate(0px, 0px);
238
+ -ms-transform: translate(0px, 0px);
239
+ transform: translate(0px, 0px); }
240
+ .tile.tile-position-1-2 {
241
+ -webkit-transform: translate(0px, 121px);
242
+ -moz-transform: translate(0px, 121px);
243
+ -ms-transform: translate(0px, 121px);
244
+ transform: translate(0px, 121px); }
245
+ .tile.tile-position-1-3 {
246
+ -webkit-transform: translate(0px, 242px);
247
+ -moz-transform: translate(0px, 242px);
248
+ -ms-transform: translate(0px, 242px);
249
+ transform: translate(0px, 242px); }
250
+ .tile.tile-position-1-4 {
251
+ -webkit-transform: translate(0px, 363px);
252
+ -moz-transform: translate(0px, 363px);
253
+ -ms-transform: translate(0px, 363px);
254
+ transform: translate(0px, 363px); }
255
+ .tile.tile-position-2-1 {
256
+ -webkit-transform: translate(121px, 0px);
257
+ -moz-transform: translate(121px, 0px);
258
+ -ms-transform: translate(121px, 0px);
259
+ transform: translate(121px, 0px); }
260
+ .tile.tile-position-2-2 {
261
+ -webkit-transform: translate(121px, 121px);
262
+ -moz-transform: translate(121px, 121px);
263
+ -ms-transform: translate(121px, 121px);
264
+ transform: translate(121px, 121px); }
265
+ .tile.tile-position-2-3 {
266
+ -webkit-transform: translate(121px, 242px);
267
+ -moz-transform: translate(121px, 242px);
268
+ -ms-transform: translate(121px, 242px);
269
+ transform: translate(121px, 242px); }
270
+ .tile.tile-position-2-4 {
271
+ -webkit-transform: translate(121px, 363px);
272
+ -moz-transform: translate(121px, 363px);
273
+ -ms-transform: translate(121px, 363px);
274
+ transform: translate(121px, 363px); }
275
+ .tile.tile-position-3-1 {
276
+ -webkit-transform: translate(242px, 0px);
277
+ -moz-transform: translate(242px, 0px);
278
+ -ms-transform: translate(242px, 0px);
279
+ transform: translate(242px, 0px); }
280
+ .tile.tile-position-3-2 {
281
+ -webkit-transform: translate(242px, 121px);
282
+ -moz-transform: translate(242px, 121px);
283
+ -ms-transform: translate(242px, 121px);
284
+ transform: translate(242px, 121px); }
285
+ .tile.tile-position-3-3 {
286
+ -webkit-transform: translate(242px, 242px);
287
+ -moz-transform: translate(242px, 242px);
288
+ -ms-transform: translate(242px, 242px);
289
+ transform: translate(242px, 242px); }
290
+ .tile.tile-position-3-4 {
291
+ -webkit-transform: translate(242px, 363px);
292
+ -moz-transform: translate(242px, 363px);
293
+ -ms-transform: translate(242px, 363px);
294
+ transform: translate(242px, 363px); }
295
+ .tile.tile-position-4-1 {
296
+ -webkit-transform: translate(363px, 0px);
297
+ -moz-transform: translate(363px, 0px);
298
+ -ms-transform: translate(363px, 0px);
299
+ transform: translate(363px, 0px); }
300
+ .tile.tile-position-4-2 {
301
+ -webkit-transform: translate(363px, 121px);
302
+ -moz-transform: translate(363px, 121px);
303
+ -ms-transform: translate(363px, 121px);
304
+ transform: translate(363px, 121px); }
305
+ .tile.tile-position-4-3 {
306
+ -webkit-transform: translate(363px, 242px);
307
+ -moz-transform: translate(363px, 242px);
308
+ -ms-transform: translate(363px, 242px);
309
+ transform: translate(363px, 242px); }
310
+ .tile.tile-position-4-4 {
311
+ -webkit-transform: translate(363px, 363px);
312
+ -moz-transform: translate(363px, 363px);
313
+ -ms-transform: translate(363px, 363px);
314
+ transform: translate(363px, 363px); }
315
+
316
+ .tile {
317
+ position: absolute;
318
+ -webkit-transition: 100ms ease-in-out;
319
+ -moz-transition: 100ms ease-in-out;
320
+ transition: 100ms ease-in-out;
321
+ -webkit-transition-property: -webkit-transform;
322
+ -moz-transition-property: -moz-transform;
323
+ transition-property: transform; }
324
+ .tile .tile-inner {
325
+ border-radius: 3px;
326
+ background: #eee4da;
327
+ text-align: center;
328
+ font-weight: bold;
329
+ z-index: 10;
330
+ font-size: 55px; }
331
+ .tile.tile-2 .tile-inner {
332
+ background: #eee4da;
333
+ box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0); }
334
+ .tile.tile-4 .tile-inner {
335
+ background: #ede0c8;
336
+ box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0), inset 0 0 0 1px rgba(255, 255, 255, 0); }
337
+ .tile.tile-8 .tile-inner {
338
+ color: #f9f6f2;
339
+ background: #f2b179; }
340
+ .tile.tile-16 .tile-inner {
341
+ color: #f9f6f2;
342
+ background: #f59563; }
343
+ .tile.tile-32 .tile-inner {
344
+ color: #f9f6f2;
345
+ background: #f67c5f; }
346
+ .tile.tile-64 .tile-inner {
347
+ color: #f9f6f2;
348
+ background: #f65e3b; }
349
+ .tile.tile-128 .tile-inner {
350
+ color: #f9f6f2;
351
+ background: #edcf72;
352
+ box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.2381), inset 0 0 0 1px rgba(255, 255, 255, 0.14286);
353
+ font-size: 45px; }
354
+ @media screen and (max-width: 520px) {
355
+ .tile.tile-128 .tile-inner {
356
+ font-size: 25px; } }
357
+ .tile.tile-256 .tile-inner {
358
+ color: #f9f6f2;
359
+ background: #edcc61;
360
+ box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.31746), inset 0 0 0 1px rgba(255, 255, 255, 0.19048);
361
+ font-size: 45px; }
362
+ @media screen and (max-width: 520px) {
363
+ .tile.tile-256 .tile-inner {
364
+ font-size: 25px; } }
365
+ .tile.tile-512 .tile-inner {
366
+ color: #f9f6f2;
367
+ background: #edc850;
368
+ box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.39683), inset 0 0 0 1px rgba(255, 255, 255, 0.2381);
369
+ font-size: 45px; }
370
+ @media screen and (max-width: 520px) {
371
+ .tile.tile-512 .tile-inner {
372
+ font-size: 25px; } }
373
+ .tile.tile-1024 .tile-inner {
374
+ color: #f9f6f2;
375
+ background: #edc53f;
376
+ box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.47619), inset 0 0 0 1px rgba(255, 255, 255, 0.28571);
377
+ font-size: 35px; }
378
+ @media screen and (max-width: 520px) {
379
+ .tile.tile-1024 .tile-inner {
380
+ font-size: 15px; } }
381
+ .tile.tile-2048 .tile-inner {
382
+ color: #f9f6f2;
383
+ background: #edc22e;
384
+ box-shadow: 0 0 30px 10px rgba(243, 215, 116, 0.55556), inset 0 0 0 1px rgba(255, 255, 255, 0.33333);
385
+ font-size: 35px; }
386
+ @media screen and (max-width: 520px) {
387
+ .tile.tile-2048 .tile-inner {
388
+ font-size: 15px; } }
389
+ .tile.tile-super .tile-inner {
390
+ color: #f9f6f2;
391
+ background: #3c3a32;
392
+ font-size: 30px; }
393
+ @media screen and (max-width: 520px) {
394
+ .tile.tile-super .tile-inner {
395
+ font-size: 10px; } }
396
+
397
+ @-webkit-keyframes appear {
398
+ 0% {
399
+ opacity: 0;
400
+ -webkit-transform: scale(0);
401
+ -moz-transform: scale(0);
402
+ -ms-transform: scale(0);
403
+ transform: scale(0); }
404
+
405
+ 100% {
406
+ opacity: 1;
407
+ -webkit-transform: scale(1);
408
+ -moz-transform: scale(1);
409
+ -ms-transform: scale(1);
410
+ transform: scale(1); } }
411
+ @-moz-keyframes appear {
412
+ 0% {
413
+ opacity: 0;
414
+ -webkit-transform: scale(0);
415
+ -moz-transform: scale(0);
416
+ -ms-transform: scale(0);
417
+ transform: scale(0); }
418
+
419
+ 100% {
420
+ opacity: 1;
421
+ -webkit-transform: scale(1);
422
+ -moz-transform: scale(1);
423
+ -ms-transform: scale(1);
424
+ transform: scale(1); } }
425
+ @keyframes appear {
426
+ 0% {
427
+ opacity: 0;
428
+ -webkit-transform: scale(0);
429
+ -moz-transform: scale(0);
430
+ -ms-transform: scale(0);
431
+ transform: scale(0); }
432
+
433
+ 100% {
434
+ opacity: 1;
435
+ -webkit-transform: scale(1);
436
+ -moz-transform: scale(1);
437
+ -ms-transform: scale(1);
438
+ transform: scale(1); } }
439
+ .tile-new .tile-inner {
440
+ -webkit-animation: appear 200ms ease 100ms;
441
+ -moz-animation: appear 200ms ease 100ms;
442
+ animation: appear 200ms ease 100ms;
443
+ -webkit-animation-fill-mode: backwards;
444
+ -moz-animation-fill-mode: backwards;
445
+ animation-fill-mode: backwards; }
446
+
447
+ @-webkit-keyframes pop {
448
+ 0% {
449
+ -webkit-transform: scale(0);
450
+ -moz-transform: scale(0);
451
+ -ms-transform: scale(0);
452
+ transform: scale(0); }
453
+
454
+ 50% {
455
+ -webkit-transform: scale(1.2);
456
+ -moz-transform: scale(1.2);
457
+ -ms-transform: scale(1.2);
458
+ transform: scale(1.2); }
459
+
460
+ 100% {
461
+ -webkit-transform: scale(1);
462
+ -moz-transform: scale(1);
463
+ -ms-transform: scale(1);
464
+ transform: scale(1); } }
465
+ @-moz-keyframes pop {
466
+ 0% {
467
+ -webkit-transform: scale(0);
468
+ -moz-transform: scale(0);
469
+ -ms-transform: scale(0);
470
+ transform: scale(0); }
471
+
472
+ 50% {
473
+ -webkit-transform: scale(1.2);
474
+ -moz-transform: scale(1.2);
475
+ -ms-transform: scale(1.2);
476
+ transform: scale(1.2); }
477
+
478
+ 100% {
479
+ -webkit-transform: scale(1);
480
+ -moz-transform: scale(1);
481
+ -ms-transform: scale(1);
482
+ transform: scale(1); } }
483
+ @keyframes pop {
484
+ 0% {
485
+ -webkit-transform: scale(0);
486
+ -moz-transform: scale(0);
487
+ -ms-transform: scale(0);
488
+ transform: scale(0); }
489
+
490
+ 50% {
491
+ -webkit-transform: scale(1.2);
492
+ -moz-transform: scale(1.2);
493
+ -ms-transform: scale(1.2);
494
+ transform: scale(1.2); }
495
+
496
+ 100% {
497
+ -webkit-transform: scale(1);
498
+ -moz-transform: scale(1);
499
+ -ms-transform: scale(1);
500
+ transform: scale(1); } }
501
+ .tile-merged .tile-inner {
502
+ z-index: 20;
503
+ -webkit-animation: pop 200ms ease 100ms;
504
+ -moz-animation: pop 200ms ease 100ms;
505
+ animation: pop 200ms ease 100ms;
506
+ -webkit-animation-fill-mode: backwards;
507
+ -moz-animation-fill-mode: backwards;
508
+ animation-fill-mode: backwards; }
509
+
510
+ .above-game:after {
511
+ content: "";
512
+ display: block;
513
+ clear: both; }
514
+
515
+ .game-intro {
516
+ float: left;
517
+ line-height: 42px;
518
+ margin-bottom: 0; }
519
+
520
+ .restart-button {
521
+ display: inline-block;
522
+ background: #8f7a66;
523
+ border-radius: 3px;
524
+ padding: 0 20px;
525
+ text-decoration: none;
526
+ color: #f9f6f2;
527
+ height: 40px;
528
+ line-height: 42px;
529
+ display: block;
530
+ text-align: center;
531
+ float: right; }
532
+
533
+ .game-explanation {
534
+ margin-top: 50px; }
535
+
536
+ .ai-controls {
537
+ margin: 15px 0;
538
+ padding: 15px;
539
+ background: #eee4da;
540
+ border-radius: 6px; }
541
+
542
+ .ai-controls .controls-row {
543
+ display: flex;
544
+ gap: 10px;
545
+ align-items: center;
546
+ flex-wrap: wrap;
547
+ margin-bottom: 10px; }
548
+
549
+ .ai-controls label {
550
+ font-size: 14px;
551
+ color: #776e65;
552
+ font-weight: bold;
553
+ display: flex;
554
+ align-items: center;
555
+ gap: 6px; }
556
+
557
+ .ai-controls select {
558
+ padding: 6px 10px;
559
+ border-radius: 3px;
560
+ border: 2px solid #bbada0;
561
+ font-size: 14px;
562
+ background: white;
563
+ cursor: pointer; }
564
+
565
+ .ai-controls .ai-btn {
566
+ display: inline-block;
567
+ border-radius: 3px;
568
+ padding: 0 20px;
569
+ color: #f9f6f2;
570
+ height: 40px;
571
+ line-height: 42px;
572
+ cursor: pointer;
573
+ font-weight: bold;
574
+ font-size: 14px;
575
+ user-select: none; }
576
+
577
+ .ai-start-button {
578
+ background: #8f7a66; }
579
+
580
+ .ai-stop-button {
581
+ background: #bbada0; }
582
+
583
+ .ai-controls .max-rng-checkbox {
584
+ width: 18px;
585
+ height: 18px;
586
+ cursor: pointer; }
587
+
588
+ .ai-controls label.checkbox-label {
589
+ cursor: pointer; }
590
+
591
+ .ai-status {
592
+ font-size: 13px;
593
+ color: #776e65;
594
+ font-weight: bold;
595
+ min-height: 18px; }
596
+
597
+ @media screen and (max-width: 520px) {
598
+ html, body {
599
+ font-size: 15px; }
600
+
601
+ body {
602
+ margin: 20px 0;
603
+ padding: 0 20px; }
604
+
605
+ h1.title {
606
+ font-size: 27px;
607
+ margin-top: 15px; }
608
+
609
+ .container {
610
+ width: 280px;
611
+ margin: 0 auto; }
612
+
613
+ .score-container, .best-container {
614
+ margin-top: 0;
615
+ padding: 15px 10px;
616
+ min-width: 40px; }
617
+
618
+ .heading {
619
+ margin-bottom: 10px; }
620
+
621
+ .game-intro {
622
+ width: 55%;
623
+ display: block;
624
+ box-sizing: border-box;
625
+ line-height: 1.65; }
626
+
627
+ .restart-button {
628
+ width: 42%;
629
+ padding: 0;
630
+ display: block;
631
+ box-sizing: border-box;
632
+ margin-top: 2px; }
633
+
634
+ .game-container {
635
+ margin-top: 17px;
636
+ position: relative;
637
+ padding: 10px;
638
+ cursor: default;
639
+ -webkit-touch-callout: none;
640
+ -ms-touch-callout: none;
641
+ -webkit-user-select: none;
642
+ -moz-user-select: none;
643
+ -ms-user-select: none;
644
+ -ms-touch-action: none;
645
+ touch-action: none;
646
+ background: #bbada0;
647
+ border-radius: 6px;
648
+ width: 280px;
649
+ height: 280px;
650
+ -webkit-box-sizing: border-box;
651
+ -moz-box-sizing: border-box;
652
+ box-sizing: border-box; }
653
+ .game-container .game-message {
654
+ display: none;
655
+ position: absolute;
656
+ top: 0;
657
+ right: 0;
658
+ bottom: 0;
659
+ left: 0;
660
+ background: rgba(238, 228, 218, 0.5);
661
+ z-index: 100;
662
+ text-align: center;
663
+ -webkit-animation: fade-in 800ms ease 1200ms;
664
+ -moz-animation: fade-in 800ms ease 1200ms;
665
+ animation: fade-in 800ms ease 1200ms;
666
+ -webkit-animation-fill-mode: both;
667
+ -moz-animation-fill-mode: both;
668
+ animation-fill-mode: both; }
669
+ .game-container .game-message p {
670
+ font-size: 60px;
671
+ font-weight: bold;
672
+ height: 60px;
673
+ line-height: 60px;
674
+ margin-top: 222px; }
675
+ .game-container .game-message .lower {
676
+ display: block;
677
+ margin-top: 59px; }
678
+ .game-container .game-message a {
679
+ display: inline-block;
680
+ background: #8f7a66;
681
+ border-radius: 3px;
682
+ padding: 0 20px;
683
+ text-decoration: none;
684
+ color: #f9f6f2;
685
+ height: 40px;
686
+ line-height: 42px;
687
+ margin-left: 9px; }
688
+ .game-container .game-message a.keep-playing-button {
689
+ display: none; }
690
+ .game-container .game-message.game-won {
691
+ background: rgba(237, 194, 46, 0.5);
692
+ color: #f9f6f2; }
693
+ .game-container .game-message.game-won a.keep-playing-button {
694
+ display: inline-block; }
695
+ .game-container .game-message.game-won, .game-container .game-message.game-over {
696
+ display: block; }
697
+
698
+ .grid-container {
699
+ position: absolute;
700
+ z-index: 1; }
701
+
702
+ .grid-row {
703
+ margin-bottom: 10px; }
704
+ .grid-row:last-child {
705
+ margin-bottom: 0; }
706
+ .grid-row:after {
707
+ content: "";
708
+ display: block;
709
+ clear: both; }
710
+
711
+ .grid-cell {
712
+ width: 57.5px;
713
+ height: 57.5px;
714
+ margin-right: 10px;
715
+ float: left;
716
+ border-radius: 3px;
717
+ background: rgba(238, 228, 218, 0.35); }
718
+ .grid-cell:last-child {
719
+ margin-right: 0; }
720
+
721
+ .tile-container {
722
+ position: absolute;
723
+ z-index: 2; }
724
+
725
+ .tile, .tile .tile-inner {
726
+ width: 58px;
727
+ height: 58px;
728
+ line-height: 58px; }
729
+ .tile.tile-position-1-1 {
730
+ -webkit-transform: translate(0px, 0px);
731
+ -moz-transform: translate(0px, 0px);
732
+ -ms-transform: translate(0px, 0px);
733
+ transform: translate(0px, 0px); }
734
+ .tile.tile-position-1-2 {
735
+ -webkit-transform: translate(0px, 67px);
736
+ -moz-transform: translate(0px, 67px);
737
+ -ms-transform: translate(0px, 67px);
738
+ transform: translate(0px, 67px); }
739
+ .tile.tile-position-1-3 {
740
+ -webkit-transform: translate(0px, 135px);
741
+ -moz-transform: translate(0px, 135px);
742
+ -ms-transform: translate(0px, 135px);
743
+ transform: translate(0px, 135px); }
744
+ .tile.tile-position-1-4 {
745
+ -webkit-transform: translate(0px, 202px);
746
+ -moz-transform: translate(0px, 202px);
747
+ -ms-transform: translate(0px, 202px);
748
+ transform: translate(0px, 202px); }
749
+ .tile.tile-position-2-1 {
750
+ -webkit-transform: translate(67px, 0px);
751
+ -moz-transform: translate(67px, 0px);
752
+ -ms-transform: translate(67px, 0px);
753
+ transform: translate(67px, 0px); }
754
+ .tile.tile-position-2-2 {
755
+ -webkit-transform: translate(67px, 67px);
756
+ -moz-transform: translate(67px, 67px);
757
+ -ms-transform: translate(67px, 67px);
758
+ transform: translate(67px, 67px); }
759
+ .tile.tile-position-2-3 {
760
+ -webkit-transform: translate(67px, 135px);
761
+ -moz-transform: translate(67px, 135px);
762
+ -ms-transform: translate(67px, 135px);
763
+ transform: translate(67px, 135px); }
764
+ .tile.tile-position-2-4 {
765
+ -webkit-transform: translate(67px, 202px);
766
+ -moz-transform: translate(67px, 202px);
767
+ -ms-transform: translate(67px, 202px);
768
+ transform: translate(67px, 202px); }
769
+ .tile.tile-position-3-1 {
770
+ -webkit-transform: translate(135px, 0px);
771
+ -moz-transform: translate(135px, 0px);
772
+ -ms-transform: translate(135px, 0px);
773
+ transform: translate(135px, 0px); }
774
+ .tile.tile-position-3-2 {
775
+ -webkit-transform: translate(135px, 67px);
776
+ -moz-transform: translate(135px, 67px);
777
+ -ms-transform: translate(135px, 67px);
778
+ transform: translate(135px, 67px); }
779
+ .tile.tile-position-3-3 {
780
+ -webkit-transform: translate(135px, 135px);
781
+ -moz-transform: translate(135px, 135px);
782
+ -ms-transform: translate(135px, 135px);
783
+ transform: translate(135px, 135px); }
784
+ .tile.tile-position-3-4 {
785
+ -webkit-transform: translate(135px, 202px);
786
+ -moz-transform: translate(135px, 202px);
787
+ -ms-transform: translate(135px, 202px);
788
+ transform: translate(135px, 202px); }
789
+ .tile.tile-position-4-1 {
790
+ -webkit-transform: translate(202px, 0px);
791
+ -moz-transform: translate(202px, 0px);
792
+ -ms-transform: translate(202px, 0px);
793
+ transform: translate(202px, 0px); }
794
+ .tile.tile-position-4-2 {
795
+ -webkit-transform: translate(202px, 67px);
796
+ -moz-transform: translate(202px, 67px);
797
+ -ms-transform: translate(202px, 67px);
798
+ transform: translate(202px, 67px); }
799
+ .tile.tile-position-4-3 {
800
+ -webkit-transform: translate(202px, 135px);
801
+ -moz-transform: translate(202px, 135px);
802
+ -ms-transform: translate(202px, 135px);
803
+ transform: translate(202px, 135px); }
804
+ .tile.tile-position-4-4 {
805
+ -webkit-transform: translate(202px, 202px);
806
+ -moz-transform: translate(202px, 202px);
807
+ -ms-transform: translate(202px, 202px);
808
+ transform: translate(202px, 202px); }
809
+
810
+ .tile .tile-inner {
811
+ font-size: 35px; }
812
+
813
+ .game-message p {
814
+ font-size: 30px !important;
815
+ height: 30px !important;
816
+ line-height: 30px !important;
817
+ margin-top: 90px !important; }
818
+ .game-message .lower {
819
+ margin-top: 30px !important; } }