Tutorial: Snake
Snake se mueve en una cuadrícula, celda a celda, así que no necesita un bucle por
fotograma: basta un temporizador que salte unas cuantas veces por segundo. Aprenderás a
guardar el estado del juego en la app, avanzarlo con pz.every, girar con on_key, dibujar
el tablero en un canvas y poner un menú encima con pz.stack.
| Objetivo | Tiempo | Nivel | Necesitas |
|---|---|---|---|
| El Snake clásico: una cuadrícula, un temporizador, una cola de teclas y una tarjeta de game over | 30 minutos | Intermedio | El tutorial de la Lista de tareas, o algunas apps de Pyzed propias |
Juégalo primero:
uv run --with pyzed-ui python examples/games/snake.pypython3 -m venv .venvsource .venv/bin/activatepython -m pip install pyzed-uipython examples/games/snake.pypy -m venv .venv.venv\Scripts\Activate.ps1python -m pip install pyzed-uipython examples\games\snake.pypython3 examples/games/snake.pypy examples\games\snake.py-
El estado. La serpiente es una lista de celdas, empezando por la cabeza. La dirección es un par
(dx, dy)yturnsguarda las pulsaciones hasta el siguiente paso (si no, dos pulsaciones rápidas entre pasos perderían la primera):CELL = 22COLS, ROWS = 24, 18class Snake(pz.App):title = "Snake"size = (COLS * CELL + 32, ROWS * CELL + 96)def __init__(self, seed=None):self.random = random.Random(seed) # una semilla hace las partidas reproducibles en testsself.best = 0self.reset()def reset(self):middle = (COLS // 2, ROWS // 2)self.body = [middle, (middle[0] - 1, middle[1]), (middle[0] - 2, middle[1])]self.direction = (1, 0)self.turns = []self.food = self.free_cell()self.score = 0self.paused = self.over = False -
Un paso. Mueve la cabeza; si cae en la comida la serpiente crece (la cola se queda), si no, la cola avanza también. Las paredes dan la vuelta con
%y morderse a sí misma termina la partida:def step(self):if self.turns:self.direction = self.turns.pop(0)dx, dy = self.directionx, y = self.body[0]head = ((x + dx) % COLS, (y + dy) % ROWS)if head in self.body[:-1]:self.over = Trueself.best = max(self.best, self.score)returnself.body.insert(0, head)if head == self.food:self.score += 1self.food = self.free_cell()else:self.body.pop() -
El temporizador.
subscriptions()se vuelve a leer tras cada actualización, así que el juego decide cuándo corre el reloj. Poner la velocidad en lakeyreinicia el temporizador cuando la serpiente acelera, y no devolver nada lo pausa:@propertydef speed(self):return min(8 + self.score // 3, 20) # pasos por segundodef subscriptions(self):if self.over or self.paused:return []return [pz.every(1 / self.speed, self.step, key=f"snake-{self.speed}")] -
Girar.
on_keyrecibe cada pulsación como texto. Se ignora dar media vuelta (o pulsar dos veces la misma dirección):DIRECTIONS = {"up": (0, -1), "w": (0, -1), "down": (0, 1), "s": (0, 1),"left": (-1, 0), "a": (-1, 0), "right": (1, 0), "d": (1, 0)}def on_key(self, combo):if combo in DIRECTIONS:last = self.turns[-1] if self.turns else self.directionnew = DIRECTIONS[combo]if new != last and (new[0] + last[0], new[1] + last[1]) != (0, 0):self.turns.append(new)elif combo == "space" and not self.over:self.paused = not self.pausedelif combo == "enter" and self.over:self.reset() -
El tablero. Un canvas es una lista de formas de
pyzed.draw. El damero es una sola formadraw.rects(rápida incluso con cientos de celdas), la comida tiene un halo suave hecho con un círculo translúcido y el cuerpo son rectángulos redondeados:def board(self, t):shapes = [draw.background(t.editor)]shapes.append(draw.rects(((x * CELL, y * CELL, CELL, CELL)for x in range(COLS) for y in range(ROWS) if (x + y) % 2),fill=t.surface))fx, fy = self.foodshapes.append(draw.circle(fx * CELL + CELL / 2, fy * CELL + CELL / 2, CELL / 2 - 3,fill="#d07277"))for i, (x, y) in enumerate(self.body):shapes.append(draw.rect(x * CELL + 2, y * CELL + 2, CELL - 4, CELL - 4, radius=6,fill=t.accent if i == 0 else "#a1c181"))return pz.canvas(shapes, width=COLS * CELL, height=ROWS * CELL) -
HUD y game over. La puntuación es una fila de
ui.badge. La tarjeta de game over es un contenedor normal conglass=True(un panel tintado que desenfoca lo de detrás), apilado sobre el tablero:def overlay(self, t):if not self.over:return Nonecard = pz.container(pz.column([ui.heading("Game over", level=2),ui.muted(f"Score {self.score} · Best {self.best}"),ui.button("Play again", self.reset, variant="solid", icon="rotate-ccw"),], spacing=10, align="center"), padding=24).style(glass=True, radius=14)return pz.container(card, width="fill", height="fill", align_x="center", align_y="center")def view(self):game = pz.stack([self.board(t), self.overlay(t)], width=COLS * CELL, height=ROWS * CELL)
Código completo
Sección titulada «Código completo»"""Snake: the classic, in about 150 lines of Pyzed.
python examples/games/snake.py
Arrows or WASD steer, Space pauses, Enter starts again. Shows a fixed-rate game loopwith ``pz.every``, steering from ``on_key`` (with a queue so quick turns are not lost),a ``pz.canvas`` board and a HUD made of ordinary widgets stacked on top of it.Tutorial: https://albakore.github.io/pyzed/games/snake/"""
import random
import pyzed as pzfrom pyzed import draw, ui
CELL = 22COLS, ROWS = 24, 18DIRECTIONS = { "up": (0, -1), "w": (0, -1), "down": (0, 1), "s": (0, 1), "left": (-1, 0), "a": (-1, 0), "right": (1, 0), "d": (1, 0),}
class Snake(pz.App): title = "Snake" size = (COLS * CELL + 32, ROWS * CELL + 96) theme = "One Dark"
def __init__(self, seed=None): self.random = random.Random(seed) self.best = 0 self.reset()
# -- game state ----------------------------------------------------------- def reset(self): middle = (COLS // 2, ROWS // 2) self.body = [middle, (middle[0] - 1, middle[1]), (middle[0] - 2, middle[1])] self.direction = (1, 0) self.turns = [] # key presses waiting for the next step self.food = self.free_cell() self.score = 0 self.paused = False self.over = False
def free_cell(self): while True: cell = (self.random.randrange(COLS), self.random.randrange(ROWS)) if cell not in self.body: return cell
@property def speed(self): """Steps per second: starts at 8 and speeds up as the snake grows.""" return min(8 + self.score // 3, 20)
def step(self): if self.turns: self.direction = self.turns.pop(0) dx, dy = self.direction x, y = self.body[0] head = ((x + dx) % COLS, (y + dy) % ROWS) # walls wrap around if head in self.body[:-1]: self.over = True self.best = max(self.best, self.score) return self.body.insert(0, head) if head == self.food: self.score += 1 self.food = self.free_cell() else: self.body.pop()
# -- input and timing ----------------------------------------------------- def subscriptions(self): if self.over or self.paused: return [] # The key includes the speed so the timer restarts when it changes. return [pz.every(1 / self.speed, self.step, key=f"snake-{self.speed}")]
def on_key(self, combo): if combo in DIRECTIONS: last = self.turns[-1] if self.turns else self.direction new = DIRECTIONS[combo] # Ignore turning back on itself and repeated presses. if new != last and (new[0] + last[0], new[1] + last[1]) != (0, 0) and len(self.turns) < 3: self.turns.append(new) elif combo == "space" and not self.over: self.paused = not self.paused elif combo == "enter" and self.over: self.reset()
# -- drawing -------------------------------------------------------------- def board(self, t): shapes = [draw.background(t.editor)] # A subtle checkerboard makes the grid readable without lines. shapes.append(draw.rects(((x * CELL, y * CELL, CELL, CELL) for x in range(COLS) for y in range(ROWS) if (x + y) % 2), fill=t.surface)) fx, fy = self.food shapes.append(draw.circle(fx * CELL + CELL / 2, fy * CELL + CELL / 2, CELL * 0.62, fill=(0.82, 0.45, 0.47, 0.18))) shapes.append(draw.circle(fx * CELL + CELL / 2, fy * CELL + CELL / 2, CELL / 2 - 3, fill="#d07277")) for i, (x, y) in enumerate(self.body): inset = 2 if i else 1 shapes.append(draw.rect(x * CELL + inset, y * CELL + inset, CELL - 2 * inset, CELL - 2 * inset, radius=6 if i else 7, fill=t.accent if i == 0 else "#a1c181")) # Eyes looking where the snake goes. hx, hy = self.body[0] dx, dy = self.direction cx, cy = hx * CELL + CELL / 2 + dx * 4, hy * CELL + CELL / 2 + dy * 4 for side in (-1, 1): shapes.append(draw.circle(cx + dy * 5 * side, cy + dx * 5 * side, 2.2, fill=t.editor)) return pz.canvas(shapes, width=COLS * CELL, height=ROWS * CELL)
def overlay(self, t): if self.over: title, hint = "Game over", "Press Enter to play again" elif self.paused: title, hint = "Paused", "Press Space to continue" else: return None card = pz.container(pz.column([ ui.heading(title, level=2), ui.muted(f"Score {self.score} · Best {self.best}"), ui.button(hint.split(" to ")[1].capitalize(), self.reset if self.over else (lambda: setattr(self, "paused", False)), variant="solid", icon="rotate-ccw" if self.over else "play"), ui.muted(hint, size="sm"), ], spacing=10, align="center"), padding=24).style(glass=True, radius=14, border=(1, "border")) return pz.container(card, width="fill", height="fill", align_x="center", align_y="center")
def view(self): t = self.resolved_theme() hud = pz.row([ pz.icon("worm", size=18, color=t.accent), ui.label("Snake", weight="semibold"), pz.hspace(), ui.badge(f"Score {self.score}", icon="apple"), ui.badge(f"Best {self.best}", tint="warning", icon="trophy"), ui.badge(f"{self.speed}/s", tint="text_muted", icon="gauge"), ], spacing=8, align="center") game = pz.stack([self.board(t), self.overlay(t)], width=COLS * CELL, height=ROWS * CELL) return pz.container( pz.column([hud, pz.container(game, radius=10, clip=True), ui.muted( "Arrows / WASD steer · Space pauses · Enter restarts", size="sm")], spacing=10), padding=16, width="fill", height="fill", bg=t.background, )
if __name__ == "__main__": pz.run(Snake)