Ir al contenido
Ayotl

← Así se hace un juego: Snake de Daily Challenge · Lección 1/16

Qué vamos a desarmar

✕
1/7

Un reto al día

Daily Challenge pone un juego arcade nuevo cada día, al estilo de la Atari 2600: una partida por persona, un tablero del día y, a medianoche, el top 10. Tiene 36 juegos: 29 para el reto diario y 7 versus para jugar con amigos.

Snake es de los más cortos y el más conocido: unas 180 líneas en un solo archivo, games/snake.js. Por eso es el que vamos a desarmar. El tablero es una cuadrícula de 16 × 17 casillas; las x del mapa son muros, y la serpiente come frutas para crecer.

El programa de abajo lee ese mapa tal como viene en el archivo de texto snake_01.txt y cuenta lo que hay.

  • x muro
  • s cuerpo
  • c cabeza
  • f fruta
leccion.js

$ node leccion.js

Pulsa CORRER (o Ctrl+Enter).

Qué mirar

Un nivel entero es un arreglo de textos. La p marca la salida de la serpiente (columna 7, renglón 8). Los mapas viven en archivos .txt aparte del código: hacer un nivel nuevo es escribir un dibujo, no programar.

Juega Snake en Daily Challenge ↗

Toda la lección en una página

Un reto al día

Daily Challenge pone un juego arcade nuevo cada día, al estilo de la Atari 2600: una partida por persona, un tablero del día y, a medianoche, el top 10. Tiene 36 juegos: 29 para el reto diario y 7 versus para jugar con amigos.

Snake es de los más cortos y el más conocido: unas 180 líneas en un solo archivo, games/snake.js. Por eso es el que vamos a desarmar. El tablero es una cuadrícula de 16 × 17 casillas; las x del mapa son muros, y la serpiente come frutas para crecer.

El programa de abajo lee ese mapa tal como viene en el archivo de texto snake_01.txt y cuenta lo que hay.

  • x muro
  • s cuerpo
  • c cabeza
  • f fruta
// El mapa snake_01.txt, tal cual (16 columnas x 17 renglones)
const mapa = [
  "xxxxxxxxxxxxxxxx",
  "x..............x",
  "x..xx......xx..x",
  "x..xx......xx..x",
  "x..............x",
  "x..............x",
  "x..............x",
  "x..............x",
  "x......p.......x",
  "x..............x",
  "x..............x",
  "x..............x",
  "x..............x",
  "x..xx......xx..x",
  "x..xx......xx..x",
  "x..............x",
  "xxxxxxxxxxxxxxxx",
];

let muros = 0, libres = 0;
for (const renglon of mapa)
  for (const c of renglon)
    if (c === "x") muros++; else libres++;

console.log("renglones:", mapa.length, "columnas:", mapa[0].length);
console.log("muros:", muros, "libres:", libres);
const y = mapa.findIndex(r => r.includes("p"));
console.log("la serpiente sale en", { x: mapa[y].indexOf("p"), y });

Un nivel entero es un arreglo de textos. La p marca la salida de la serpiente (columna 7, renglón 8). Los mapas viven en archivos .txt aparte del código: hacer un nivel nuevo es escribir un dibujo, no programar.

Sin motor y sin compilar

No hay Unity, ni Phaser, ni un paso de compilación. Cada juego es un módulo de JavaScript que importa lo que necesita, y el navegador lo carga tal cual:

import { BTN } from '../core/buttons.js';

La razón más fuerte es otra: el mismo archivo corre en el servidor. Cuando terminas una partida, tu teléfono no manda tu puntaje: manda la lista de botones que apretaste en cada momento. El servidor (Node.js) importa el mismo snake.js, vuelve a jugar la partida con esos botones y saca el puntaje él solo. Si el juego dependiera de un motor que sólo vive en el navegador, eso no se podría.

El programa simula las dos partes con el mismo código.

// Un "juego" mínimo: suma puntos si aprietas A (16)
class Juego {
  constructor() { this.score = 0; this.over = false; this.tick = 0; }
  update(mask) {
    this.tick++;
    if (mask & 16) this.score += 10;
    if (this.tick >= 6) this.over = true;
  }
}

// En el teléfono: se juega y se guarda cada botón
const log = [0, 16, 0, 16, 16, 0];
const telefono = new Juego();
for (const m of log) telefono.update(m);

// En el servidor: llega SOLO el log, y se vuelve a jugar
const servidor = new Juego();
for (const m of log) servidor.update(m);

console.log("teléfono:", telefono.score, "servidor:", servidor.score);

Los dos dan 30. El servidor nunca vio la pantalla ni recibió un número de puntos: lo calculó. Esa idea sostiene todo el curso, y la unidad del servidor la cuenta completa.

Lo que el lenguaje da y lo que no

El navegador trae casi todo lo que un juego arcade necesita:

  • Canvas 2D para pintar píxeles.
  • Web Audio para generar sonido con osciladores, sin archivos de audio.
  • Eventos de teclado y toque (pointerdown, keydown).
  • requestAnimationFrame para dibujar al ritmo de la pantalla.
  • Conexiones en vivo para jugar en línea.

Y tiene tres trampas que el código de Daily Challenge esquiva a propósito:

  • Un solo hilo: si un cálculo tarda, la pantalla se congela. Por eso cada paso del juego es corto.
  • Números con decimales que redondean: 0.1 + 0.2 no da 0.3 exacto. Para que el teléfono y el servidor saquen lo mismo, el azar se hace con enteros de 32 bits (Math.imul, >>> 0) y no con Math.random.
  • El recolector de basura: crear objetos nuevos en cada cuadro causa tirones. Snake vacía su lista de eventos con this.events.length = 0 en vez de crear una nueva.
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);

// Con enteros de 32 bits no hay redondeo: da igual en todos lados
console.log(Math.imul(123456789, 987654321));
console.log((4000000000 + 500000000) >>> 0);

// Math.random cambia en cada corrida (y no se le puede dar semilla)
console.log(Math.random() === Math.random());

// Vaciar sin crear un arreglo nuevo
const events = ["eat", "die"];
const mismo = events;
events.length = 0;
console.log(mismo, mismo === events);
  • 0.30000000000000004: el redondeo de los decimales.
  • Math.imul multiplica como lo haría C con enteros de 32 bits, y >>> 0 deja el número sin signo: resultados exactos en cualquier máquina.
  • Vaciar con length = 0 deja el mismo arreglo (true), sin basura nueva.