código·java·oracle
Java y JVM

El juego de la serpiente en Java: un clásico para aprender

Un tablero, una serpiente que crece y un bucle de juego a 10 fotogramas: el proyecto perfecto para entender hilos, colas y el paso del tiempo en Java.

publicado noviembre de 2011 revisado agosto de 2026 661 palabras
JAVAPunto.java
// Punto.java -- una celda del tablero
public record Punto(int x, int y) {
    public Punto mover(Dir d) {
        return switch (d) {
            case ARRIBA    -> new Punto(x, y - 1);
            case ABAJO     -> new Punto(x, y + 1);
            case IZQUIERDA -> new Punto(x - 1, y);
            case DERECHA   -> new Punto(x + 1, y);
        };
    }
}

El juego de la serpiente es el «hola mundo» de los videojuegos: reglas de una línea —comes, creces; chocas, mueres— y todo el meollo técnico en superficie. En 2011 este sitio publicó una versión beta con AWT y applets; los applets murieron, pero el esqueleto didáctico sigue siendo el proyecto perfecto para entender cómo una programa maneja el tiempo, el estado y la entrada a la vez. Esta es la reconstrucción moderna, compilable con cualquier JDK 17.

Viejo monitor CRT con una retícula verde difusa y un serpenteo de píxeles brillantes, teclado mecánico de los 2000 delante
El taller de 2011: CRT, retícula verde y una serpiente de píxeles.

El corazón: el bucle de juego

Todos los juegos de tablero comparten la misma estructura: actualizar estado, repintar, esperar un tick, repetir. En Java, un hilo con Thread.sleep sigue siendo la forma más clara de verlo:

JAVABucleJuego.java
private static final int TICK_MS = 90;   // ~11 fotogramas/seg

public void run() {
    while (enMarcha) {
        long inicio = System.nanoTime();
        actualizar();
        repintar();
        long empleado = (System.nanoTime() - inicio) / 1_000_000;
        try {
            long pausa = TICK_MS - empleado;
            if (pausa > 0) Thread.sleep(pausa);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            enMarcha = false;
        }
    }
}

El detalle elegante es medir cuánto ha costado actualizar y restarlo de la espera: el tick real queda clavado a 90 ms aunque el repintado tarde 15. Y el detalle de adulto es restaurar el flag de interrupción en el catch: un InterruptedException tragado en silencio es la vía rápida a hilos zombis que ni viven ni mueren.

Estado: la serpiente es una cola

Una serpiente es exactamente una estructura de datos: una Deque donde la cabeza entra por un extremo y la cola sale por el otro —salvo cuando comes, que la cola se queda. Con el record Punto de arriba, la actualización completa son seis líneas:

JAVASerpiente.java
private final Deque<Punto> serpiente = new ArrayDeque<>();
private Punto comida;

void actualizar() {
    Punto nuevaCabeza = serpiente.peekFirst().mover(direccion);

    if (nuevaCabeza.x() < 0 || nuevaCabeza.x() >= ANCHO
        || nuevaCabeza.y() < 0 || nuevaCabeza.y() >= ALTO
        || serpiente.contains(nuevaCabeza)) {
        enMarcha = false;                 // pared o auto-collision
        return;
    }
    serpiente.addFirst(nuevaCabeza);
    if (nuevaCabeza.equals(comida)) {
        puntuacion++;
        colocarComida();                  // crece: no quitamos cola
    } else {
        serpiente.removeLast();
    }
}

contains sobre una cola es O(n), y aquí está bien: cien celdas no justifican un HashSet. Saber cuándo la optimización es innecesaria es parte del aprendizaje.

Entrada: el teclado y la dirección prohibida

JAVATeclado.java
addKeyListener(new KeyAdapter() {
    @Override public void keyPressed(KeyEvent e) {
        Dir nueva = switch (e.getKeyCode()) {
            case KeyEvent.VK_UP    -> Dir.ARRIBA;
            case KeyEvent.VK_DOWN  -> Dir.ABAJO;
            case KeyEvent.VK_LEFT  -> Dir.IZQUIERDA;
            case KeyEvent.VK_RIGHT -> Dir.DERECHA;
            default -> direccion;
        };
        // prohibido darse la vuelta sobre uno mismo
        if (nueva.opuesta() != direccion) direccion = nueva;
    }
});

El guardián de la dirección opuesta no es un capricho: sin él, un doble pulsación rápida en pleno tick haría a la serpiente entrar en su propio cuello y morir por un error de interfaz, no del jugador. Toda la game-feel vive en detalles así.

Dibujar: Canvas y nada más

JAVATablero.java
@Override protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.setColor(new Color(0x15171c));
    g.fillRect(0, 0, getWidth(), getHeight());

    g.setColor(new Color(0xf89820));           // serpiente
    for (Punto p : serpiente)
        g.fillRect(p.x() * CELDA, p.y() * CELDA, CELDA - 1, CELDA - 1);

    g.setColor(new Color(0xc74634));           // comida
    Punto c = comida;
    g.fillOval(c.x() * CELDA, c.y() * CELDA, CELDA - 1, CELDA - 1);
}

Sobre un JPanel con paintComponent, el dibujado entero no llega a diez líneas. El -1 en el tamaño de la celda deja un hilo de separación entre segmentos: cuatro píxeles que convierten un blob en una serpiente reconocible.

Lo que se aprende de verdad

  • Inmutabilidad: el record Punto y el switch que devuelve nuevos puntos en vez de mutar —la serpiente nunca edita su cuerpo, lo reemplaza.
  • Hilos con educación: sleep interrumpible, flag de parada, interrupción restaurada.
  • Estado y vista separados: actualizar no sabe dibujar, paintComponent no decide; la misma lección que separa un juego de juguete de una arquitectura decente.

De aquí saltan dos caminos naturales: profundizar en el lenguaje con la reflexión y sus alternativas, o el histórico puente Java-JavaScript, que era el otro gran truco de la época —y sigue siendo una buena historia.