Compartir en Twitter
Go to Homepage

CONSTRUYE UN JUEGO DE SERPIENTE EN JAVASCRIPT PASO A PASO

October 19, 2025

Introducción al Desarrollo del Juego de Serpiente

En el mundo del desarrollo web, crear juegos simples representa una excelente oportunidad para practicar conceptos fundamentales de programación interactiva. Este tutorial guía a los lectores a través de la construcción de un juego de serpiente clásico, similar al icónico título del Nokia 3310, utilizando únicamente HTML, CSS y JavaScript. Actualizado al contexto actual de 2025, donde las aplicaciones web progresivas y los entornos de desarrollo ágiles dominan el panorama tecnológico, este enfoque sin frameworks externos enfatiza la pureza del código vanilla, fomentando una comprensión profunda de los mecanismos del DOM y los eventos del navegador.

El juego consiste en una serpiente que se desplaza por un tablero cuadrado, consumiendo manzanas para crecer en longitud mientras evita chocar contra las paredes o contra sí misma. Cada consumo acelera el movimiento, incrementando la dificultad progresivamente. Este proyecto no solo entretiene, sino que ilustra principios como la manipulación dinámica del DOM, el manejo de intervalos temporizados y la detección de colisiones en un entorno bidimensional discreto. Para programadores emergentes o aquellos interesados en noticias tecnológicas, este ejercicio demuestra cómo tecnologías maduras como JavaScript continúan evolucionando para soportar experiencias inmersivas sin dependencias pesadas.

Comenzaremos estableciendo la estructura base con HTML y CSS, avanzando luego hacia la lógica central en JavaScript. Cada sección incluye ejemplos de código comentados para mayor claridad, permitiendo a los lectores replicar el desarrollo en sus editores locales. Al finalizar, los participantes habrán adquirido habilidades transferibles a proyectos más complejos, como simulaciones físicas o interfaces de usuario responsivas.

Estructura HTML del Juego de Serpiente

La base de cualquier aplicación web radica en su markup semántico, y en este caso, el HTML define los contenedores esenciales para el tablero, puntuación y controles. Consideremos el siguiente esqueleto, que prioriza la simplicidad y accesibilidad:

<h1>Nokia 3310 Snake</h1>
<div class="scoreDisplay"></div>
<div class="grid"></div>
<div class="button">
    <button class="top">Arriba</button>
    <button class="bottom">Abajo</button>
    <button class="left">Izquierda</button>
    <button class="right">Derecha</button>
</div>
<div class="popup">
    <button class="playAgain">Jugar de Nuevo</button>
</div>

Este fragmento establece un encabezado descriptivo, un div para mostrar la puntuación actual, el contenedor principal del tablero de juego con clase grid, un grupo de botones direccionales adaptados para interacción táctil en dispositivos móviles, y un popup oculto inicialmente que activa la opción de reinicio al finalizar la partida. La clase grid servirá como lienzo de 10 por 10 celdas, generando dinámicamente 100 elementos div mediante JavaScript para simular el espacio de juego.

En el contexto de sitios web de programación, esta aproximación minimalista resalta la importancia de la accesibilidad: los botones incluyen etiquetas claras, facilitando el uso con lectores de pantalla, mientras que el diseño responsivo se abordará en CSS. Actualizando al 2025, incorporamos consideraciones de rendimiento, como evitar elementos innecesarios que podrían impactar en dispositivos de bajo consumo, comunes en el ecosistema móvil actual.

Para visualizar esta estructura inicial, los divs del grid se crearán en runtime, pero el HTML proporciona el esqueleto robusto que soporta toda la interacción subsiguiente. Este paso inicial asegura que el código sea legible y mantenible, principios clave en el desarrollo moderno de software.

Estilos CSS para el Tablero y Elementos Visuales

Una vez definido el HTML, los estilos CSS transforman esta estructura en una interfaz visual atractiva y funcional. El siguiente bloque de CSS establece dimensiones fijas para el tablero, colores distintivos para la serpiente y la manzana, y posicionamiento para el popup de reinicio:

body {
    background: rgb(212, 211, 211);
}

.grid {
    width: 200px;
    height: 200px;
    border: 1px solid red;
    margin: 0 auto;
    display: flex;
    flex-wrap: wrap;
}

.grid div {
    width: 20px;
    height: 20px;
}

.snake {
    background: blue;
}

.apple {
    background: yellow;
    border-radius: 20px;
}

.popup {
    background: rgb(32, 31, 31);
    width: 100px;
    height: 100px;
    position: fixed;
    top: 100px;
    left: 100px;
    display: flex;
    justify-content: center;
    align-items: center;
}

Aquí, el contenedor .grid adopta un tamaño de 200x200 píxeles, delimitado por un borde rojo para resaltar los límites del juego, y utiliza display: flex con flex-wrap: wrap para alinear las 100 celdas en una cuadrícula perfecta. Cada celda individual, representada por div hijo, mide 20x20 píxeles, asegurando un ajuste preciso sin solapamientos.

La clase .snake asigna un fondo azul a las porciones de la serpiente, mientras que .apple emplea amarillo con bordes redondeados para evocar una fruta jugosa, mejorando la estética retro. El .popup, posicionado fijo en la esquina superior izquierda, centra su botón de reinicio mediante flexbox, ocultándose inicialmente vía JavaScript. En 2025, estos estilos incorporan sombras suaves o transiciones opcionales para modernizar la experiencia, aunque mantenemos la simplicidad para enfocarnos en la lógica.

Para depuración, se puede descomentar temporalmente bordes en .grid div (como border: 1px black solid; box-sizing: border-box;), revelando la cuadrícula subyacente. Esta configuración no solo define la apariencia, sino que soporta el flujo de datos dinámico, donde JavaScript inyecta clases para actualizar visualmente el estado del juego en tiempo real.

Variables Iniciales en JavaScript para el Control del Juego

Transicionando a la lógica, JavaScript maneja el estado dinámico mediante variables globales que capturan referencias DOM y parámetros del juego. El siguiente ejemplo declara estas entidades esenciales:

let grid = document.querySelector(".grid");
let popup = document.querySelector(".popup");
let playAgain = document.querySelector(".playAgain");
let scoreDisplay = document.querySelector(".scoreDisplay");
let left = document.querySelector(".left");
let bottom = document.querySelector(".bottom");
let right = document.querySelector(".right");
let up = document.querySelector(".top");
let width = 10;
let currentIndex = 0;
let appleIndex = 0;
let currentSnake = [2, 1, 0];
let direction = 1;
let score = 0;
let speed = 0.8;
let intervalTime = 0;
let interval = 0;

Estas declaraciones seleccionan elementos clave del DOM al inicio, definiendo width como la dimensión de la cuadrícula (10 celdas por lado), currentSnake como un array representando las posiciones iniciales de la serpiente (empezando en índices 2, 1, 0 para una orientación horizontal), y direction en 1 para movimiento inicial a la derecha. Variables como score rastrean el progreso, mientras intervalTime y speed controlan la cadencia de actualizaciones, acelerándose al consumir manzanas.

En el panorama tecnológico actual, esta modularidad facilita la escalabilidad: por ejemplo, ajustar width a 20 generaría un tablero más grande, requiriendo recalcular colisiones. La serpiente crece al comer manzanas, un mecanismo que ilustra arrays mutables en JavaScript, y currentIndex sirve como puntero al segmento frontal. Esta inicialización prepara el terreno para funciones que manipularán estos valores, asegurando un estado consistente durante la ejecución.

Configuración de Eventos al Cargar el Documento

Para sincronizar la inicialización con el renderizado del navegador, empleamos el evento DOMContentLoaded. Este listener orquesta la creación del tablero, inicio del juego y bindings de controles:

document.addEventListener("DOMContentLoaded", function () {
    document.addEventListener("keyup", control);
    createBoard();
    startGame();
    playAgain.addEventListener("click", replay);
});

Al cargar el contenido, se adjunta un listener para teclas de flecha vía keyup, llamando a control para redirigir la serpiente. Posteriormente, createBoard genera las celdas, startGame lanza la simulación, y replay se vincula al botón de reinicio. En 2025, con navegadores optimizados para carga asíncrona, este enfoque previene errores de elementos no encontrados, mejorando la robustez.

Este patrón de eventos centraliza la lógica de inicialización, permitiendo extensiones como pausas o guardado de progreso en localStorage, común en aplicaciones web modernas.

Creación Dinámica del Tablero de Juego

La función createBoard popula el contenedor .grid con 100 divs, formando la cuadrícula jugable y ocultando el popup inicial:

function createBoard() {
    popup.style.display = "none";
    for (let i = 0; i < 100; i++) {
        let div = document.createElement("div");
        grid.appendChild(div);
    }
}

El bucle for itera 100 veces (10x10), creando y anexando elementos div que heredan los estilos CSS para dimensionamiento uniforme. Ocultar el popup asegura una pantalla limpia al inicio. Para claridad, imagina este proceso como la inicialización de un array bidimensional en memoria, donde cada índice corresponde a una coordenada (fila, columna) calculada como fila * width + columna.

En contextos de noticias tecnológicas, esta generación dinámica optimiza el rendimiento al evitar markup estático voluminoso, alineándose con prácticas de renderizado lazy en frameworks como React, aunque aquí mantenemos vanilla para pureza educativa.

Iniciación del Juego con Posicionamiento Inicial

La función startGame resetea parámetros, coloca la manzana y lanza el intervalo de movimiento:

function startGame() {
    let squares = document.querySelectorAll(".grid div");
    randomApple(squares);
    direction = 1;
    scoreDisplay.innerHTML = score;
    intervalTime = 1000;
    currentSnake = [2, 1, 0];
    currentIndex = 0;
    currentSnake.forEach((index) => squares[index].classList.add("snake"));
    interval = setInterval(moveOutcome, intervalTime);
}

Selecciona todas las celdas en squares, invoca randomApple para posicionar la fruta, reinicia dirección y puntuación a cero, y establece un intervalo de 1000ms. El array currentSnake se repuebla, aplicando la clase snake a cada posición vía forEach. Finalmente, setInterval ejecuta moveOutcome periódicamente, simulando el avance continuo.

Este ritual de inicio encapsula el estado basal, permitiendo reinicios fluidos. En actualizaciones de 2025, se podría integrar Web Animations API para transiciones suaves, pero el timer básico prioriza simplicidad y control preciso.

Lógica de Movimiento y Resultados en Cada Intervalo

Cada tick del intervalo evalúa colisiones en moveOutcome, deteniendo el juego si es necesario o avanzando la serpiente:

function moveOutcome() {
    let squares = document.querySelectorAll(".grid div");
    if (checkForHits(squares)) {
        alert("Has chocado con algo");
        popup.style.display = "flex";
        return clearInterval(interval);
    } else {
        moveSnake(squares);
    }
}

Obtiene las celdas, chequea impactos con checkForHits; si true, alerta al usuario, muestra el popup y limpia el intervalo. De lo contrario, delega a moveSnake. Esta bifurcación condicional forma el núcleo del loop de juego, equilibrando verificación y actualización en un solo ciclo.

Para depuración, console.log en cada rama revela flujos, útil en entornos de desarrollo como VS Code con extensiones Live Server.

Actualización de la Posición de la Serpiente

La función moveSnake desplaza el array representando la serpiente, removiendo la cola y agregando una cabeza nueva:

function moveSnake(squares) {
    let tail = currentSnake.pop();
    squares[tail].classList.remove("snake");
    currentSnake.unshift(currentSnake[0] + direction);
    eatApple(squares, tail);
    squares[currentSnake[0]].classList.add("snake");
}

Extrae la cola con pop, limpia su clase visual, calcula la nueva cabeza sumando direction al índice frontal y la inserta con unshift. Luego, verifica consumo de manzana y actualiza la clase en la nueva posición. Este shift eficiente mantiene la ilusión de movimiento continuo, con complejidad O(1) por operación.

Ejemplo: Si currentSnake = [2,1,0] y direction=1, pop da tail=0, unshift(2+1=3) resulta en [3,2,1], visualizando el avance derecho. En programación de juegos, este patrón de deque simula colas FIFO, fundamental para entidades móviles.

Detección de Colisiones con Bordes y Cuerpo

checkForHits inspecciona condiciones terminales, retornando true si la próxima cabeza viola límites o se superpone:

function checkForHits(squares) {
    if (
        (currentSnake[0] + width >= width * width && direction === width) ||
        (currentSnake[0] % width === width - 1 && direction === 1) ||
        (currentSnake[0] % width === 0 && direction === -1) ||
        (currentSnake[0] - width <= 0 && direction === -width) ||
        squares[currentSnake[0] + direction].classList.contains("snake")
    ) {
        return true;
    } else {
        return false;
    }
}

Evalúa cinco escenarios: impacto inferior (cabeza + width >= 100 y bajando), derecho (resto width-1 y derecha), izquierdo (resto 0 y izquierda), superior (cabeza - width <=0 y subiendo), y auto-colisión (próxima celda tiene snake). Cada cláusula usa aritmética modular para coordenadas, traduciendo índices lineales a grid 2D.

Por instancia, en posición 97 bajando: 97+10=107 >100, true. Esta lógica determinista previene avances inválidos, esencial en simulaciones discretas. Evitar choques contra paredes es clave para la inmersión, y en 2025, algoritmos similares se usan en IA para pathfinding en juegos complejos.

Mecanismo de Consumo de Manzanas y Crecimiento

Al avanzar, eatApple detecta y procesa la ingesta, extendiendo la longitud y acelerando:

function eatApple(squares, tail) {
    if (squares[currentSnake[0]].classList.contains("apple")) {
        squares[currentSnake[0]].classList.remove("apple");
        squares[tail].classList.add("snake");
        currentSnake.push(tail);
        randomApple(squares);
        score++;
        scoreDisplay.textContent = score;
        clearInterval(interval);
        intervalTime = intervalTime * speed;
        interval = setInterval(moveOutcome, intervalTime);
    }
}

Si la cabeza ocupa una celda con apple, la remueve, restaura la cola como nuevo segmento con push(tail), reposiciona la fruta, incrementa puntuación y ajusta velocidad multiplicando intervalTime por speed (0.8 para desaceleración relativa, pero en práctica acelera al reducir tiempo). Limpia y reinicia el intervalo para aplicar el cambio inmediato.

Este bucle de retroalimentación positiva aumenta desafío, con crecimiento lineal por manzana. En términos de noticias tech, refleja gamificación en apps, donde progresión motiva retención de usuarios.

Generación Aleatoria de Posiciones para Manzanas

randomApple selecciona un índice libre de serpiente usando un do-while para evitar superposiciones:

function randomApple(squares) {
    do {
        appleIndex = Math.floor(Math.random() * squares.length);
    } while (squares[appleIndex].classList.contains("snake"));
    squares[appleIndex].classList.add("apple");
}

Genera un entero aleatorio entre 0-99, verifica ausencia de snake, y aplica la clase si válido; el loop persiste hasta éxito. Math.random() asegura uniformidad, previniendo manzanas inaccesibles. Para variantes, se podría ponderar probabilidades cerca de la serpiente, pero esta simplicidad basta para el núcleo.

En desarrollo web, esta aleatoriedad introduce replayability, un factor clave en engagement según métricas de analytics modernas.

Implementación de Controles por Teclado

Para usuarios de escritorio, control mapea flechas a direcciones vía keyCode:

function control(e) {
    if (e.keyCode === 39) {
        direction = 1; // derecha
    } else if (e.keyCode === 38) {
        direction = -width; // arriba
    } else if (e.keyCode === 37) {
        direction = -1; // izquierda
    } else if (e.keyCode === 40) {
        direction = width; // abajo
    }
}

Escucha keyup en documento, asignando 1/-1 para horizontal, ±width para vertical, reflejando saltos de fila. Nota: En 2025, keyCode depreca; alternativamente, usa e.key como ‘ArrowRight’, pero mantenemos compatibilidad legacy aquí.

Esta función reactiva permite giros fluidos, con latencia mínima en navegadores optimizados.

Controles Táctiles para Dispositivos Móviles

Paralelamente, eventos click en botones actualizan dirección para touch:

up.addEventListener("click", () => (direction = -width));
bottom.addEventListener("click", () => (direction = width));
left.addEventListener("click", () => (direction = -1));
right.addEventListener("click", () => (direction = 1));

Cada listener arrow function asigna directamente, simplificando el binding. Para prevenir giros inválidos (e.g., 180°), se podría agregar lógica de validación, pero el básico funciona para prototipos.

En el auge de PWAs en 2025, estos controles aseguran cross-platform, alineando con touch gestures en tablets.

Funcionalidad de Reinicio de Partida

Finalmente, replay restaura el estado inicial al clickear el botón:

function replay() {
    grid.innerHTML = "";
    createBoard();
    startGame();
    popup.style.display = "none";
}

Limpia el grid con innerHTML="", regenera celdas, reinicia juego y oculta popup. Este reset atómico previene residuos visuales, asegurando frescura.

Conclusiones

Este tutorial ha desglosado la creación de un juego de serpiente completo en JavaScript, desde la estructura HTML hasta la lógica de colisiones y controles duales. Al dominar estos elementos, los desarrolladores adquieren herramientas versátiles para prototipos interactivos, destacando la potencia de vanilla JS en 2025. Experimenta modificaciones como power-ups o multiplayer local para extender el aprendizaje, contribuyendo a un portafolio robusto en programación web.