# Tutorial: Snake

> Crea el clásico Snake con un temporizador fijo, una cola de teclas, un tablero en canvas y una tarjeta de cristal para el game over.

Source: https://albakore.github.io/pyzed/es/games/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](https://albakore.github.io/pyzed/es/tutorials/basic/todo-app/), o algunas apps de Pyzed propias |

Juégalo primero:

1. **El estado.** La serpiente es una lista de celdas, empezando por la cabeza. La dirección
   es un par `(dx, dy)` y `turns` guarda las pulsaciones hasta el siguiente paso (si no, dos
   pulsaciones rápidas entre pasos perderían la primera):

```python
CELL = 22
COLS, ROWS = 24, 18

class 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 tests
        self.best = 0
        self.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 = 0
        self.paused = self.over = False
```

2. **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:

```python
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)
    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()
```

3. **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 la `key` reinicia el
   temporizador cuando la serpiente acelera, y no devolver nada lo pausa:

```python
@property
def speed(self):
    return min(8 + self.score // 3, 20)      # pasos por segundo

def subscriptions(self):
    if self.over or self.paused:
        return []
    return [pz.every(1 / self.speed, self.step, key=f"snake-{self.speed}")]
```

4. **Girar.** `on_key` recibe cada pulsación como texto. Se ignora dar media vuelta (o pulsar
   dos veces la misma dirección):

```python
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.direction
        new = 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.paused
    elif combo == "enter" and self.over:
        self.reset()
```

5. **El tablero.** Un canvas es una lista de formas de `pyzed.draw`. El damero es una sola
   forma `draw.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:

```python
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.food
    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):
        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)
```

6. **HUD y game over.** La puntuación es una fila de `ui.badge`. La tarjeta de game over es un
   contenedor normal con `glass=True` (un panel tintado que desenfoca lo de detrás), apilado
   sobre el tablero:

```python
def overlay(self, t):
    if not self.over:
        return None
    card = 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)
```

   (Image: Game over, en una tarjeta de cristal)

**Tip:**

Ideas para seguir: paredes que matan en lugar de dar la vuelta, manzanas doradas que valen
tres puntos y desaparecen a los pocos segundos (un segundo `pz.every`), o un modo de dos
jugadores con WASD y las flechas.

## Código completo

`examples/games/snake.py`:

```py
"""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 loop
with ``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 pz
from pyzed import draw, ui

CELL = 22
COLS, ROWS = 24, 18
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),
}

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)
```
