← Así se hace un juego: Snake de Daily Challenge · Lección 7/16
El cuerpo es una cola
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.
$ 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.
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.