Spaces:
Runtime error
Runtime error
File size: 7,211 Bytes
b92f470 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | # Neon Snake — Gradio Edition
# Works on Hugging Face Spaces (Gradio Space)
# Controls: Arrow buttons or WASD in the small textbox (press Enter)
import random
from typing import Dict, List, Tuple
from PIL import Image, ImageDraw, ImageFont
import gradio as gr
# ---------- Visual + Game Constants ----------
GRID = 24 # number of cells in each dimension
CELL = 18 # pixel size of a cell
BORDER = 12 # outer border size
W = H = GRID * CELL + BORDER * 2
NEON_BG = (11, 15, 26)
GRID_COL = (35, 60, 139)
SNAKE = (95, 168, 255)
HEAD = (255, 255, 255)
FOOD = (139, 233, 253)
TXT = (167, 195, 255)
START_SPEED = 0.14 # seconds per tick (lower = faster)
SPEED_STEP = 0.01 # how much to speed up each fruit
MIN_SPEED = 0.06 # cap so it doesn't get too fast
DIRS = {
"UP": (0, -1),
"DOWN": (0, 1),
"LEFT": (-1, 0),
"RIGHT": (1, 0),
}
OPPOSITE = {
"UP": "DOWN", "DOWN": "UP", "LEFT": "RIGHT", "RIGHT": "LEFT"
}
# ---------- Game Logic ----------
def new_game() -> Dict:
snake = [(GRID // 2, GRID // 2)]
direction = "RIGHT"
food = place_food(snake)
return {
"snake": snake,
"direction": direction,
"pending_dir": direction, # queued turn to apply next tick
"food": food,
"alive": True,
"score": 0,
"high": 0,
"paused": False,
"speed": START_SPEED,
}
def place_food(snake: List[Tuple[int, int]]) -> Tuple[int, int]:
free = [(x, y) for x in range(GRID) for y in range(GRID) if (x, y) not in snake]
return random.choice(free) if free else (0, 0)
def step(state: Dict) -> Dict:
if not state["alive"] or state["paused"]:
return state
# apply any pending turn (disallow reversal into self)
if state["pending_dir"] != OPPOSITE[state["direction"]]:
state["direction"] = state["pending_dir"]
dx, dy = DIRS[state["direction"]]
head_x, head_y = state["snake"][0]
new_head = (head_x + dx, head_y + dy)
# collide with walls
if not (0 <= new_head[0] < GRID and 0 <= new_head[1] < GRID):
state["alive"] = False
state["high"] = max(state["high"], state["score"])
return state
# collide with self
if new_head in state["snake"]:
state["alive"] = False
state["high"] = max(state["high"], state["score"])
return state
# move
state["snake"].insert(0, new_head)
# eat?
if new_head == state["food"]:
state["score"] += 1
state["food"] = place_food(state["snake"])
state["speed"] = max(MIN_SPEED, state["speed"] - SPEED_STEP)
else:
state["snake"].pop()
return state
# ---------- Rendering ----------
def render(state: Dict) -> Image.Image:
img = Image.new("RGB", (W, H), NEON_BG)
draw = ImageDraw.Draw(img)
# grid
for i in range(GRID + 1):
x = BORDER + i * CELL
y = BORDER + i * CELL
draw.line([(BORDER, y), (W - BORDER, y)], fill=GRID_COL, width=1)
draw.line([(x, BORDER), (x, H - BORDER)], fill=GRID_COL, width=1)
# food
fx, fy = state["food"]
draw_cell(draw, fx, fy, FOOD, radius=6)
# snake
for i, (sx, sy) in enumerate(state["snake"]):
color = HEAD if i == 0 else SNAKE
draw_cell(draw, sx, sy, color, radius=6)
# HUD
status = "PAUSED" if state["paused"] else ("GAME OVER" if not state["alive"] else "NEON SNAKE")
hud = f"{status} | Score: {state['score']} High: {state['high']}"
draw.text((BORDER, 4), hud, fill=TXT)
return img
def draw_cell(draw: ImageDraw.ImageDraw, cx: int, cy: int, col, radius=4):
x0 = BORDER + cx * CELL
y0 = BORDER + cy * CELL
x1 = x0 + CELL
y1 = y0 + CELL
# rounded rectangle
draw.rounded_rectangle([x0 + 2, y0 + 2, x1 - 2, y1 - 2], radius=radius, fill=col)
# ---------- UI Callbacks ----------
def on_key(state: Dict, key: str):
if not state["alive"]:
return state
k = (key or "").strip().upper()
# WASD mapping
if k in ["W", "UP"]:
nd = "UP"
elif k in ["S", "DOWN"]:
nd = "DOWN"
elif k in ["A", "LEFT"]:
nd = "LEFT"
elif k in ["D", "RIGHT"]:
nd = "RIGHT"
else:
return state
# don't allow immediate reversal into self
if nd != OPPOSITE[state["direction"]]:
state["pending_dir"] = nd
return state
def click_dir(state: Dict, nd: str):
# same behavior as keyboard
if not state["alive"]:
return state
if nd != OPPOSITE[state["direction"]]:
state["pending_dir"] = nd
return state
def toggle_pause(state: Dict):
if state["alive"]:
state["paused"] = not state["paused"]
return state
def reset_game(state: Dict):
high = state.get("high", 0)
ng = new_game()
ng["high"] = max(high, ng["high"])
return ng
def tick(state: Dict):
# advance game and return new frame + (possibly) updated timer interval
before = state["speed"]
state = step(state)
frame = render(state)
# if speed changed after eating, we can notify the frontend to adjust timer
return frame, state["speed"], state
# ---------- Gradio App ----------
with gr.Blocks(css="""
#wrap {max-width: 760px; margin: 0 auto;}
.button-row button {min-width: 84px;}
""") as demo:
gr.Markdown("## 🐍 Neon Snake — Gradio\nUse arrow buttons or WASD (enter) • Eat food • Don’t hit walls or yourself")
gstate = gr.State(new_game())
with gr.Row(elem_id="wrap"):
canvas = gr.Image(value=render(gstate.value), label="Game", width=W, height=H, show_download_button=False)
with gr.Row(equal_height=True, elem_classes="button-row"):
up = gr.Button("▲ Up")
left = gr.Button("◀ Left")
pause = gr.Button("⏯ Pause/Resume")
right = gr.Button("Right ▶")
down = gr.Button("Down ▼")
reset = gr.Button("🔄 Reset")
kb = gr.Textbox(label="WASD / Arrow words (e.g., 'up') then Enter", value="", placeholder="w / a / s / d or up / down / left / right")
kb_cb = gr.Button("Apply Key")
# Timer drives the game loop; interval updates dynamically as speed changes
timer = gr.Timer(interval=START_SPEED, active=True)
# --- Wire up controls ---
up.click(click_dir, [gstate], [gstate], js=None, preprocess=True, postprocess=False, kwargs={"nd": "UP"})
down.click(click_dir, [gstate], [gstate], kwargs={"nd": "DOWN"})
left.click(click_dir, [gstate], [gstate], kwargs={"nd": "LEFT"})
right.click(click_dir, [gstate], [gstate], kwargs={"nd": "RIGHT"})
pause.click(toggle_pause, [gstate], [gstate])
reset.click(reset_game, [gstate], [gstate]).then(lambda s: render(s), [gstate], [canvas])
# Keyboard textbox
def kb_apply(s: Dict, k: str):
s = on_key(s, k)
return "", s # clear the textbox for the next key
kb_cb.click(kb_apply, [gstate, kb], [kb, gstate])
# Game loop
def game_tick(state: Dict):
frame, new_interval, new_state = tick(state)
# return: image, timer interval, updated state
return frame, new_interval, new_state
timer.tick(game_tick, [gstate], [canvas, timer, gstate])
demo.queue().launch()
|