Ir al contenido
Ayotl

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

El cuerpo es una cola

✕
1/8

Una lista de casillas

La serpiente no es un dibujo: es un arreglo de casillas { x, y }, con la cabeza en la posición 0. Al empezar mide tres. El constructor de snake.js elige la dirección con más camino libre desde la salida y pone el cuerpo detrás, en sentido contrario:

this.body = [start, { x: start.x - dir0.x, y: start.y - dir0.y }, { x: start.x - 2 * dir0.x, y: start.y - 2 * dir0.y }];

Pintarla es recorrer el arreglo: la casilla 0 con el verde claro y el resto con el verde normal.

leccion.js

$ node leccion.js

Pulsa CORRER (o Ctrl+Enter).

Qué mirar

La cabeza (C) en la columna 4 y el cuerpo a su izquierda, porque mira a la derecha. Cambia dir0 a { x: 0, y: 1 } (hacia abajo) y el cuerpo queda arriba de la cabeza.

Toda la lección en una página

Una lista de casillas

La serpiente no es un dibujo: es un arreglo de casillas { x, y }, con la cabeza en la posición 0. Al empezar mide tres. El constructor de snake.js elige la dirección con más camino libre desde la salida y pone el cuerpo detrás, en sentido contrario:

this.body = [start, { x: start.x - dir0.x, y: start.y - dir0.y }, { x: start.x - 2 * dir0.x, y: start.y - 2 * dir0.y }];

Pintarla es recorrer el arreglo: la casilla 0 con el verde claro y el resto con el verde normal.

const COLS = 10, ROWS = 5;
const start = { x: 4, y: 2 }, dir0 = { x: 1, y: 0 };   // mirando a la derecha
const body = [start,
  { x: start.x - dir0.x, y: start.y - dir0.y },
  { x: start.x - 2 * dir0.x, y: start.y - 2 * dir0.y }];

function pintar(body) {
  const t = Array.from({ length: ROWS }, () => Array(COLS).fill("."));
  body.forEach((b, i) => { t[b.y][b.x] = i === 0 ? "C" : "s"; });
  for (const fila of t) console.log(fila.join(""));
}

console.log(body);
pintar(body);

La cabeza (C) en la columna 4 y el cuerpo a su izquierda, porque mira a la derecha. Cambia dir0 a { x: 0, y: 1 } (hacia abajo) y el cuerpo queda arriba de la cabeza.

Moverse: entra por delante, sale por detrás

Para avanzar no se mueve cada pedazo. Se calcula la casilla nueva de la cabeza, se mete al principio con unshift y se saca el último con pop. Todo lo de en medio se queda donde estaba, y aun así la serpiente parece reptar.

this.body.unshift(head); … this.body.pop();

Es una cola de doble extremo (deque): se mete por un lado y se saca por el otro. En un arreglo, unshift cuesta O(n) porque recorre todo para hacer lugar, pero aquí n nunca pasa de 16 × 17 = 272 casillas: no se nota.

(5,2)(4,2)(3,2)null
const COLS = 10, ROWS = 5;
const body = [{ x: 4, y: 2 }, { x: 3, y: 2 }, { x: 2, y: 2 }];
const pintar = () => {
  const t = Array.from({ length: ROWS }, () => Array(COLS).fill("."));
  body.forEach((b, i) => { t[b.y][b.x] = i === 0 ? "C" : "s"; });
  return t.map(f => f.join("")).join("  ");
};

const pasos = [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: 1 }, { x: -1, y: 0 }];
console.log("inicio  ", pintar());
for (const dir of pasos) {
  const head = { x: body[0].x + dir.x, y: body[0].y + dir.y };
  body.unshift(head);   // entra la cabeza
  body.pop();           // sale la cola
  console.log("paso    ", pintar());
}
console.log("largo:", body.length);

Cada renglón de la salida es el tablero entero aplanado (sus cinco filas una tras otra). La serpiente avanza, baja dos y da vuelta a la izquierda, y el largo sigue en 3: por cada cabeza que entra sale una cola.

Nunca de reversa

El control se lee en cada tick, pero la serpiente sólo se mueve cada interval ticks. Por eso hay dos direcciones: dir, la que lleva, y nextDir, la que pediste para el próximo paso.

if (mask & BTN.UP && this.dir.y !== 1) this.nextDir = { x: 0, y: -1 }; else if (mask & BTN.DOWN && this.dir.y !== -1) this.nextDir = { x: 0, y: 1 }; …

Cada condición prohíbe la contraria: si vas hacia abajo (dir.y === 1), arriba se ignora. Y se compara contra dir, la dirección real, no contra la pedida. Si vas a la derecha y en el mismo paso aprietas arriba y luego izquierda, la izquierda se ignora: si no, la cabeza se metería en su propio cuello.

const BTN = { UP: 1, DOWN: 2, LEFT: 4, RIGHT: 8 };
const s = { dir: { x: 1, y: 0 }, nextDir: { x: 1, y: 0 } };   // va a la derecha

function leer(mask) {
  if (mask & BTN.UP && s.dir.y !== 1) s.nextDir = { x: 0, y: -1 };
  else if (mask & BTN.DOWN && s.dir.y !== -1) s.nextDir = { x: 0, y: 1 };
  else if (mask & BTN.LEFT && s.dir.x !== 1) s.nextDir = { x: -1, y: 0 };
  else if (mask & BTN.RIGHT && s.dir.x !== -1) s.nextDir = { x: 1, y: 0 };
}

leer(BTN.LEFT);
console.log("izquierda yendo a la derecha:", s.nextDir);
leer(BTN.UP);
console.log("arriba:", s.nextDir);
leer(BTN.LEFT);
console.log("izquierda antes de moverse:", s.nextDir);
s.dir = s.nextDir;   // llega el tick en que se mueve
leer(BTN.LEFT);
console.log("izquierda ya yendo arriba:", s.nextDir);

La primera izquierda se ignora (sería reversa). Arriba se acepta. La segunda izquierda también se ignora, porque dir sigue siendo «derecha» hasta que la serpiente se mueva. Ya moviéndose hacia arriba, la izquierda sí entra.