Spaces:
Sleeping
Sleeping
Upload 4 files
Browse files- app.py +128 -0
- packages.txt +23 -0
- postBuild +3 -0
- requirements.txt +2 -0
app.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import asyncio, time
|
| 3 |
+
from pathlib import Path
|
| 4 |
+
from typing import AsyncGenerator, Tuple
|
| 5 |
+
from playwright.async_api import async_playwright
|
| 6 |
+
|
| 7 |
+
MOVE_ORDER_DEFAULT = ["ArrowUp", "ArrowLeft", "ArrowRight", "ArrowDown"]
|
| 8 |
+
|
| 9 |
+
async def get_score(page):
|
| 10 |
+
el = await page.query_selector(".score-container")
|
| 11 |
+
if not el:
|
| 12 |
+
return 0
|
| 13 |
+
text = (await el.inner_text()).strip()
|
| 14 |
+
try:
|
| 15 |
+
return int(text.split()[0].replace(",", ""))
|
| 16 |
+
except Exception:
|
| 17 |
+
return 0
|
| 18 |
+
|
| 19 |
+
async def is_game_over(page):
|
| 20 |
+
msg = await page.query_selector(".game-message.game-over, .game-over")
|
| 21 |
+
return msg is not None
|
| 22 |
+
|
| 23 |
+
async def board_signature(page):
|
| 24 |
+
tiles = await page.query_selector_all(".tile")
|
| 25 |
+
parts = []
|
| 26 |
+
for t in tiles:
|
| 27 |
+
classes = (await t.get_attribute("class")) or ""
|
| 28 |
+
parts.append(classes)
|
| 29 |
+
return "|".join(sorted(parts))
|
| 30 |
+
|
| 31 |
+
async def autoplay_stream(url:str, moves:int, fps:int, strategy:str) -> AsyncGenerator[Tuple[str,str], None]:
|
| 32 |
+
"""Yields (frame_path, logs_text) repeatedly so the first output streams frames."""
|
| 33 |
+
logs = []
|
| 34 |
+
img_path = Path("live_frame.png")
|
| 35 |
+
final_path = Path("final_frame.png")
|
| 36 |
+
move_order = MOVE_ORDER_DEFAULT.copy()
|
| 37 |
+
if strategy == "Left/Down Bias":
|
| 38 |
+
move_order = ["ArrowLeft", "ArrowDown", "ArrowRight", "ArrowUp"]
|
| 39 |
+
elif strategy == "Clockwise":
|
| 40 |
+
move_order = ["ArrowUp", "ArrowRight", "ArrowDown", "ArrowLeft"]
|
| 41 |
+
|
| 42 |
+
frame_delay_ms = max(50, int(1000 / max(1, fps)))
|
| 43 |
+
|
| 44 |
+
async with async_playwright() as p:
|
| 45 |
+
browser = await p.chromium.launch(
|
| 46 |
+
headless=True,
|
| 47 |
+
args=["--no-sandbox", "--disable-setuid-sandbox"]
|
| 48 |
+
)
|
| 49 |
+
page = await browser.new_page(viewport={"width": 900, "height": 1100})
|
| 50 |
+
await page.goto(url, wait_until="load", timeout=60000)
|
| 51 |
+
logs.append(f"Loaded: {url}")
|
| 52 |
+
yield str(img_path), "\n".join(logs)
|
| 53 |
+
|
| 54 |
+
# Make sure the game surface has focus
|
| 55 |
+
await page.mouse.click(50, 50)
|
| 56 |
+
last_sig = await board_signature(page)
|
| 57 |
+
invalid_count = 0
|
| 58 |
+
played = 0
|
| 59 |
+
last_frame_time = 0
|
| 60 |
+
|
| 61 |
+
for i in range(moves):
|
| 62 |
+
await page.keyboard.press(move_order[i % 4])
|
| 63 |
+
await page.wait_for_timeout(60)
|
| 64 |
+
|
| 65 |
+
sig = await board_signature(page)
|
| 66 |
+
if sig == last_sig:
|
| 67 |
+
for alt in move_order[1:]:
|
| 68 |
+
await page.keyboard.press(alt)
|
| 69 |
+
await page.wait_for_timeout(50)
|
| 70 |
+
new_sig = await board_signature(page)
|
| 71 |
+
if new_sig != sig:
|
| 72 |
+
sig = new_sig
|
| 73 |
+
break
|
| 74 |
+
else:
|
| 75 |
+
invalid_count += 1
|
| 76 |
+
else:
|
| 77 |
+
invalid_count = 0
|
| 78 |
+
|
| 79 |
+
last_sig = sig
|
| 80 |
+
played += 1
|
| 81 |
+
sc = await get_score(page)
|
| 82 |
+
logs.append(f"Move {played:03d} | Score {sc}")
|
| 83 |
+
# throttle logs to last 60 lines
|
| 84 |
+
if len(logs) > 60:
|
| 85 |
+
logs = logs[-60:]
|
| 86 |
+
|
| 87 |
+
# Stream a frame at desired fps
|
| 88 |
+
now = time.time()*1000
|
| 89 |
+
if now - last_frame_time >= frame_delay_ms:
|
| 90 |
+
await page.screenshot(path=str(img_path), full_page=True)
|
| 91 |
+
last_frame_time = now
|
| 92 |
+
yield str(img_path), "\n".join(logs)
|
| 93 |
+
|
| 94 |
+
if await is_game_over(page):
|
| 95 |
+
logs.append("Game Over detected. Stopping.")
|
| 96 |
+
break
|
| 97 |
+
|
| 98 |
+
if invalid_count >= 3:
|
| 99 |
+
first = move_order.pop(0)
|
| 100 |
+
move_order.append(first)
|
| 101 |
+
invalid_count = 0
|
| 102 |
+
logs.append(f"Rotated move order: {move_order}")
|
| 103 |
+
|
| 104 |
+
await page.screenshot(path=str(final_path), full_page=True)
|
| 105 |
+
yield str(final_path), "\n".join(logs) # final frame
|
| 106 |
+
await browser.close()
|
| 107 |
+
|
| 108 |
+
with gr.Blocks(title="Online Game Auto-Player (Live Stream)") as demo:
|
| 109 |
+
gr.Markdown("# Online Game Auto-Player (Live Stream)")
|
| 110 |
+
gr.Markdown("Plays an **ONLINE** game in a headless browser and streams frames while it plays. "
|
| 111 |
+
"Default URL is play2048.co. You can replace with any accessible game URL that reacts to arrow keys or clicks.")
|
| 112 |
+
with gr.Row():
|
| 113 |
+
url_in = gr.Textbox(label="Game URL", value="https://play2048.co")
|
| 114 |
+
with gr.Row():
|
| 115 |
+
moves = gr.Slider(50, 1200, value=300, step=10, label="Max moves")
|
| 116 |
+
fps = gr.Slider(1, 15, value=5, step=1, label="Stream FPS")
|
| 117 |
+
strat = gr.Dropdown(choices=["Up/Left Bias","Left/Down Bias","Clockwise"], value="Up/Left Bias", label="Move Strategy")
|
| 118 |
+
start = gr.Button("Start")
|
| 119 |
+
with gr.Row():
|
| 120 |
+
stream_out = gr.Image(label="Live View (frames)", streaming=True)
|
| 121 |
+
logs_out = gr.Textbox(label="Logs (tail)", lines=18)
|
| 122 |
+
gr.Markdown("**Note:** Internet access must be enabled in Space settings for online URLs. "
|
| 123 |
+
"On free (CPU Basic) Spaces this option is generally unavailable.")
|
| 124 |
+
|
| 125 |
+
start.click(fn=autoplay_stream, inputs=[url_in, moves, fps, strat], outputs=[stream_out, logs_out])
|
| 126 |
+
|
| 127 |
+
if __name__ == "__main__":
|
| 128 |
+
demo.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
|