← Así se hace un juego: Snake de Daily Challenge · Lección 8/16
Choques, comida y puntos
¿Está libre?
Antes de meter la cabeza nueva, Snake pregunta si esa casilla está libre. Esto es isFree, completa:
if (x < 0 || y < 0 || x >= COLS || y >= ROWS) return false; if (this.walls[y][x]) return false; return !this.body.some(b => b.x === x && b.y === y);
Fuera del tablero, muro o cuerpo: perdiste. Pero hay una excepción fina. La cola se va a mover en el mismo paso, así que meter la cabeza donde ahora está la cola es válido… siempre que no estés comiendo, porque al comer la cola se queda:
const hitsTail = !eating && head.x === tail.x && head.y === tail.y;
-
xmuro -
ccabeza -
scuerpo (la de la izquierda arriba es la cola)
$ node leccion.js
Pulsa CORRER (o Ctrl+Enter).
Qué mirar
La casilla de la izquierda no está libre (es cuerpo), pero es la cola: Snake deja pasar. Sin esa excepción, una serpiente de cuatro no podría dar vueltas en un cuadrito de 2 × 2.
Toda la lección en una página
¿Está libre?
Antes de meter la cabeza nueva, Snake pregunta si esa casilla está libre. Esto es isFree, completa:
if (x < 0 || y < 0 || x >= COLS || y >= ROWS) return false; if (this.walls[y][x]) return false; return !this.body.some(b => b.x === x && b.y === y);
Fuera del tablero, muro o cuerpo: perdiste. Pero hay una excepción fina. La cola se va a mover en el mismo paso, así que meter la cabeza donde ahora está la cola es válido… siempre que no estés comiendo, porque al comer la cola se queda:
const hitsTail = !eating && head.x === tail.x && head.y === tail.y;
-
xmuro -
ccabeza -
scuerpo (la de la izquierda arriba es la cola)
const COLS = 6, ROWS = 5;
const walls = ["xxxxxx", "x....x", "x....x", "x....x", "xxxxxx"].map(f => [...f].map(c => c === "x"));
// cabeza en (3,1), luego (3,2), (2,2) y la cola en (2,1): un cuadrito
const body = [{ x: 3, y: 1 }, { x: 3, y: 2 }, { x: 2, y: 2 }, { x: 2, y: 1 }];
function isFree(x, y) {
if (x < 0 || y < 0 || x >= COLS || y >= ROWS) return false;
if (walls[y][x]) return false;
return !body.some(b => b.x === x && b.y === y);
}
console.log("derecha (4,1):", isFree(4, 1));
console.log("arriba (3,0), muro:", isFree(3, 0));
console.log("fuera (9,9):", isFree(9, 9));
const tail = body[body.length - 1];
const head = { x: 2, y: 1 }; // izquierda: donde está la cola
console.log("izquierda (2,1):", isFree(head.x, head.y), "pero ¿es la cola?", head.x === tail.x && head.y === tail.y);
La casilla de la izquierda no está libre (es cuerpo), pero es la cola: Snake deja pasar. Sin esa excepción, una serpiente de cuatro no podría dar vueltas en un cuadrito de 2 × 2.
Comer: crecer y acelerar
Comer es no hacer pop: entra la cabeza y la cola se queda, así que la serpiente mide uno más. Además, en ese mismo paso:
this.score += 10 + Math.max(0, 8 - this.interval) * 5; // más rápido = más puntos this.interval = Math.max(3, 8 - Math.floor(this.eaten / 4)); this.events.push('eat');
Cada fruta vale 10, más 5 por cada tick que la serpiente ya va más rápido que al principio. Ir rápido es más difícil, y paga más. Y si al buscar lugar para la fruta nueva no queda ninguno libre, llenaste el tablero: la partida termina con 500 de premio.
let interval = 8, eaten = 0, score = 0;
for (let fruta = 1; fruta <= 24; fruta++) {
eaten++;
const puntos = 10 + Math.max(0, 8 - interval) * 5;
score += puntos;
if (fruta % 4 === 0) console.log("fruta", fruta, "vale", puntos, "| total", score, "| se mueve cada", interval);
interval = Math.max(3, 8 - Math.floor(eaten / 4));
}
Las primeras cuatro frutas valen 10; de la 21 en adelante, 35 cada una. Fíjate en el orden del código real: los puntos se calculan con el intervalo de antes de comer, y luego se actualiza.
Dónde sale la fruta, el hambre y el bicho
La fruta sale en una casilla libre al azar: randomEmpty junta todas las casillas libres en un arreglo y elige una con el generador de la semilla del día (this.rng.pick(free)). Como el generador es el mismo en el teléfono y en el servidor, la fruta sale en el mismo lugar en los dos.
Dos reglas más, sacadas de Nokia y de una revisión de partidas:
- Hambre:
HAMBRE = 30 * 30, 30 segundos de ticks. Si no comes en ese tiempo, pierdes. Un comentario del código cuenta que sin ella, dar vueltas en un cuadrito no se acababa nunca. - Bicho de premio: cada 5 frutas aparece lejos de la cabeza y dura
BICHO_DURA = 150ticks (5 segundos). Vale50 + 5 * Math.ceil(vida / 30): de 75 si vas por él al instante a 55 en el último segundo. No alarga ni acelera; es puntaje puro.
// El generador real de core/rng.js
function mulberry32(seed) {
let a = seed >>> 0;
const next = () => {
a = (a + 0x6D2B79F5) >>> 0;
let t = a;
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
return { pick: arr => arr[Math.floor(next() * arr.length)] };
}
const libres = [];
for (let y = 1; y < 4; y++) for (let x = 1; x < 5; x++) libres.push({ x, y });
const rngTelefono = mulberry32(2026), rngServidor = mulberry32(2026);
console.log("libres:", libres.length);
console.log("teléfono:", rngTelefono.pick(libres), "servidor:", rngServidor.pick(libres));
for (const vida of [150, 120, 60, 31, 30, 1])
console.log("bicho con vida", vida, "vale", 50 + 5 * Math.ceil(vida / 30));
Con la misma semilla, el teléfono y el servidor eligen la misma casilla. Cambia una de las dos semillas y deja de coincidir: por eso la semilla del día la decide el servidor, no tu teléfono.