luqmanabban commited on
Commit
11f2446
·
verified ·
1 Parent(s): 812b467

Upload 5 files

Browse files
Files changed (5) hide show
  1. 2048.html +193 -0
  2. app.py +104 -0
  3. packages.txt +23 -0
  4. postBuild +3 -0
  5. requirements.txt +2 -0
2048.html ADDED
@@ -0,0 +1,193 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>Offline 2048</title>
7
+ <style>
8
+ body { font-family: Arial, sans-serif; background:#faf8ef; display:flex; flex-direction:column; align-items:center; }
9
+ h1 { color:#776e65; }
10
+ .score-board { display:flex; gap:12px; margin-bottom:10px; }
11
+ .score-container, .best-container { background:#bbada0; color:#fff; padding:10px 15px; border-radius:4px; min-width:90px; text-align:center; }
12
+ .game-container { position:relative; background:#bbada0; width:500px; height:500px; border-radius:6px; padding:15px; box-sizing:border-box; }
13
+ .grid { position:relative; width:100%; height:100%; display:grid; grid-template-columns:repeat(4,1fr); grid-template-rows:repeat(4,1fr); gap:15px; }
14
+ .cell, .tile { width:100%; height:100%; border-radius:6px; display:flex; align-items:center; justify-content:center; font-weight:bold; font-size:32px; }
15
+ .cell { background:#cdc1b4; }
16
+ .tile { position:absolute; }
17
+ .tile-inner { width:100%; height:100%; border-radius:6px; display:flex; align-items:center; justify-content:center; }
18
+ .controls { margin:8px 0 16px; color:#776e65; }
19
+ .game-message { position:absolute; inset:0; background:rgba(238,228,218,0.73); display:none; align-items:center; justify-content:center; font-size:40px; color:#776e65; border-radius:6px; }
20
+ .game-message.game-over { display:flex; }
21
+ </style>
22
+ </head>
23
+ <body>
24
+ <h1>Offline 2048</h1>
25
+ <div class="score-board">
26
+ <div class="score-container">0</div>
27
+ <div class="best-container">0</div>
28
+ </div>
29
+ <div class="controls">Use Arrow Keys to play (this offline copy is tailored for automation).</div>
30
+ <div class="game-container">
31
+ <div class="grid" id="grid"></div>
32
+ <div class="game-message" id="gameMessage">Game Over</div>
33
+ </div>
34
+
35
+ <script>
36
+ const gridSize = 4;
37
+ let score = 0;
38
+ let best = 0;
39
+ const gridEl = document.getElementById('grid');
40
+ const scoreEl = document.querySelector('.score-container');
41
+ const bestEl = document.querySelector('.best-container');
42
+ const msgEl = document.getElementById('gameMessage');
43
+
44
+ // board as 2D array
45
+ let board = Array.from({length:gridSize}, () => Array(gridSize).fill(0));
46
+
47
+ function setupGrid() {
48
+ gridEl.innerHTML = '';
49
+ // base cells
50
+ for (let r = 0; r < gridSize; r++) {
51
+ for (let c = 0; c < gridSize; c++) {
52
+ const cell = document.createElement('div');
53
+ cell.className = 'cell';
54
+ cell.style.gridRowStart = r+1;
55
+ cell.style.gridColumnStart = c+1;
56
+ gridEl.appendChild(cell);
57
+ }
58
+ }
59
+ renderTiles();
60
+ }
61
+
62
+ function randomEmpty() {
63
+ const empties = [];
64
+ for (let r=0; r<gridSize; r++) for (let c=0; c<gridSize; c++) if (board[r][c]===0) empties.push({r,c});
65
+ if (empties.length === 0) return null;
66
+ return empties[Math.floor(Math.random()*empties.length)];
67
+ }
68
+
69
+ function addRandomTile() {
70
+ const spot = randomEmpty();
71
+ if (!spot) return;
72
+ board[spot.r][spot.c] = Math.random() < 0.9 ? 2 : 4;
73
+ }
74
+
75
+ function tileColor(v) {
76
+ const map = {
77
+ 0:'#cdc1b4', 2:'#eee4da',4:'#ede0c8',8:'#f2b179',16:'#f59563',
78
+ 32:'#f67c5f',64:'#f65e3b',128:'#edcf72',256:'#edcc61',512:'#edc850',
79
+ 1024:'#edc53f',2048:'#edc22e'
80
+ };
81
+ return map[v] || '#3c3a32';
82
+ }
83
+
84
+ function renderTiles() {
85
+ // remove existing tiles
86
+ [...gridEl.querySelectorAll('.tile')].forEach(t => t.remove());
87
+ // draw from board
88
+ for (let r = 0; r < gridSize; r++) {
89
+ for (let c = 0; c < gridSize; c++) {
90
+ const v = board[r][c];
91
+ if (v > 0) {
92
+ const t = document.createElement('div');
93
+ t.className = `tile tile-${v} tile-position-${c+1}-${r+1}`; // x-y like original site
94
+ t.style.gridRowStart = r+1;
95
+ t.style.gridColumnStart = c+1;
96
+ const inner = document.createElement('div');
97
+ inner.className = 'tile-inner';
98
+ inner.style.background = tileColor(v);
99
+ inner.textContent = v;
100
+ t.appendChild(inner);
101
+ gridEl.appendChild(t);
102
+ }
103
+ }
104
+ }
105
+ scoreEl.textContent = score;
106
+ best = Math.max(best, score);
107
+ bestEl.textContent = best;
108
+ }
109
+
110
+ function slide(row) {
111
+ // slide non-zeros to left and merge
112
+ let arr = row.filter(v => v !== 0);
113
+ for (let i=0; i<arr.length-1; i++) {
114
+ if (arr[i] === arr[i+1]) {
115
+ arr[i] *= 2;
116
+ score += arr[i];
117
+ arr[i+1] = 0;
118
+ i++;
119
+ }
120
+ }
121
+ arr = arr.filter(v => v !== 0);
122
+ while (arr.length < gridSize) arr.push(0);
123
+ return arr;
124
+ }
125
+
126
+ function rotateRight(mat) {
127
+ const res = Array.from({length:gridSize}, () => Array(gridSize).fill(0));
128
+ for (let r=0;r<gridSize;r++) for (let c=0;c<gridSize;c++) res[c][gridSize-1-r] = mat[r][c];
129
+ return res;
130
+ }
131
+
132
+ function move(dir) {
133
+ // dir: 'left','right','up','down'
134
+ let rotated = board;
135
+ if (dir === 'up') rotated = rotateRight(board);
136
+ if (dir === 'right') rotated = rotateRight(rotateRight(board));
137
+ if (dir === 'down') rotated = rotateRight(rotateRight(rotateRight(board)));
138
+ let moved = false;
139
+ const newB = rotated.map(row => {
140
+ const before = row.slice();
141
+ const slid = slide(row);
142
+ if (JSON.stringify(before) !== JSON.stringify(slid)) moved = true;
143
+ return slid;
144
+ });
145
+ // rotate back
146
+ let result = newB;
147
+ if (dir === 'up') result = rotateRight(rotateRight(rotateRight(newB)));
148
+ if (dir === 'right') result = rotateRight(rotateRight(newB));
149
+ if (dir === 'down') result = rotateRight(newB);
150
+ if (moved) {
151
+ board = result;
152
+ addRandomTile();
153
+ renderTiles();
154
+ if (isGameOver()) showGameOver();
155
+ }
156
+ return moved;
157
+ }
158
+
159
+ function isGameOver() {
160
+ // Any empty?
161
+ for (let r=0;r<gridSize;r++) for (let c=0;c<gridSize;c++) if (board[r][c]===0) return false;
162
+ // Any merges available?
163
+ for (let r=0;r<gridSize;r++) for (let c=0;c<gridSize;c++) {
164
+ const v = board[r][c];
165
+ if (r+1<gridSize && board[r+1][c]===v) return false;
166
+ if (c+1<gridSize && board[r][c+1]===v) return false;
167
+ }
168
+ return true;
169
+ }
170
+
171
+ function showGameOver() {
172
+ msgEl.classList.add('game-over');
173
+ }
174
+
175
+ document.addEventListener('keydown', (e) => {
176
+ if (msgEl.classList.contains('game-over')) return;
177
+ if (e.key === 'ArrowLeft') move('left');
178
+ if (e.key === 'ArrowRight') move('right');
179
+ if (e.key === 'ArrowUp') move('up');
180
+ if (e.key === 'ArrowDown') move('down');
181
+ });
182
+
183
+ function init() {
184
+ score = 0;
185
+ board = Array.from({length:gridSize}, () => Array(gridSize).fill(0));
186
+ addRandomTile(); addRandomTile();
187
+ setupGrid();
188
+ msgEl.classList.remove('game-over');
189
+ }
190
+ init();
191
+ </script>
192
+ </body>
193
+ </html>
app.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from pathlib import Path
3
+ from playwright.async_api import async_playwright
4
+ import asyncio, os
5
+
6
+ MOVE_ORDER = ["ArrowUp", "ArrowLeft", "ArrowRight", "ArrowDown"]
7
+
8
+ async def get_score(page):
9
+ el = await page.query_selector(".score-container")
10
+ if not el:
11
+ return 0
12
+ text = (await el.inner_text()).strip()
13
+ try:
14
+ return int(text.split()[0].replace(",", ""))
15
+ except Exception:
16
+ return 0
17
+
18
+ async def is_game_over(page):
19
+ msg = await page.query_selector(".game-message.game-over")
20
+ return msg is not None
21
+
22
+ async def board_signature(page):
23
+ tiles = await page.query_selector_all(".tile")
24
+ parts = []
25
+ for t in tiles:
26
+ classes = (await t.get_attribute("class")) or ""
27
+ parts.append(classes)
28
+ return "|".join(sorted(parts))
29
+
30
+ async def play_offline_2048(moves: int = 200):
31
+ logs = []
32
+ img_path = Path("2048_offline_final.png")
33
+ html_path = Path(__file__).parent / "static" / "2048.html"
34
+ url = f"file://{html_path.resolve()}"
35
+
36
+ async with async_playwright() as p:
37
+ browser = await p.chromium.launch(
38
+ headless=True,
39
+ args=["--no-sandbox", "--disable-setuid-sandbox", "--allow-file-access-from-files"]
40
+ )
41
+ page = await browser.new_page(viewport={"width": 900, "height": 1100})
42
+ await page.goto(url, wait_until="load", timeout=60000)
43
+ logs.append("Offline 2048 loaded. Starting auto-play...")
44
+
45
+ game = await page.query_selector(".game-container")
46
+ if game:
47
+ await game.click()
48
+
49
+ last_sig = await board_signature(page)
50
+ invalid_count = 0
51
+ played = 0
52
+
53
+ for i in range(moves):
54
+ move = MOVE_ORDER[i % len(MOVE_ORDER)]
55
+ await page.keyboard.press(move)
56
+ await page.wait_for_timeout(60)
57
+
58
+ sig = await board_signature(page)
59
+ if sig == last_sig:
60
+ for alt in MOVE_ORDER[1:]:
61
+ await page.keyboard.press(alt)
62
+ await page.wait_for_timeout(50)
63
+ new_sig = await board_signature(page)
64
+ if new_sig != sig:
65
+ sig = new_sig
66
+ break
67
+ else:
68
+ invalid_count += 1
69
+ else:
70
+ invalid_count = 0
71
+
72
+ last_sig = sig
73
+ played += 1
74
+ score = await get_score(page)
75
+ logs.append(f"Move {played:03d} → score {score}")
76
+
77
+ if await is_game_over(page):
78
+ logs.append("Game Over detected. Stopping.")
79
+ break
80
+
81
+ if invalid_count >= 3:
82
+ first = MOVE_ORDER.pop(0)
83
+ MOVE_ORDER.append(first)
84
+ invalid_count = 0
85
+ logs.append(f"Rotated move order to escape dead pattern: {MOVE_ORDER}")
86
+
87
+ await page.screenshot(path=str(img_path), full_page=True)
88
+ await browser.close()
89
+
90
+ return str(img_path), "\n".join(logs)
91
+
92
+ async def run(moves):
93
+ return await play_offline_2048(moves=int(moves))
94
+
95
+ ui = gr.Interface(
96
+ fn=run,
97
+ inputs=gr.Slider(50, 600, value=250, step=10, label="Max moves"),
98
+ outputs=[gr.Image(label="Final Screenshot"), gr.Textbox(label="Logs", lines=15)],
99
+ title="AI Auto-Player: 2048 (Offline, Self-Contained)",
100
+ description="Playwright launches a local offline copy of 2048 bundled in the Space. Works with internet disabled."
101
+ )
102
+
103
+ if __name__ == "__main__":
104
+ ui.launch()
packages.txt ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ libnss3
2
+ libatk1.0-0
3
+ libatk-bridge2.0-0
4
+ libasound2
5
+ libx11-6
6
+ libx11-xcb1
7
+ libxcb1
8
+ libxcomposite1
9
+ libxdamage1
10
+ libxext6
11
+ libxfixes3
12
+ libxrandr2
13
+ libxshmfence1
14
+ libgbm1
15
+ libgtk-3-0
16
+ libpangocairo-1.0-0
17
+ libpango-1.0-0
18
+ libcairo2
19
+ libcups2
20
+ libdrm2
21
+ libdbus-1-3
22
+ ca-certificates
23
+ fonts-liberation
postBuild ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ #!/bin/bash
2
+ set -e
3
+ python -m playwright install chromium
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio==4.44.0
2
+ playwright==1.46.0