Spaces:
Sleeping
Sleeping
Upload 5 files
Browse files- 2048.html +193 -0
- app.py +104 -0
- packages.txt +23 -0
- postBuild +3 -0
- 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
|