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.
// 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.
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:
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:
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
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
@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 Puntoy elswitchque 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:
actualizarno sabe dibujar,paintComponentno 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.