Skip to content
Ayotl

← How a game is made: Daily Challenge's Snake · Lesson 8/16

Collisions, food and points

✕
1/7

Is it free?

Before adding the new head, Snake asks whether that cell is free. This is isFree, in full:

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);

Off the board, a wall or the body: game over. But there's a subtle exception. The tail is going to move on the same step, so putting the head where the tail is now is fine… as long as you're not eating, because when you eat the tail stays put:

const hitsTail = !eating && head.x === tail.x && head.y === tail.y;

  • x wall
  • c head
  • s body (the top-left one is the tail)
leccion.js

$ node leccion.js

Press RUN (or Ctrl+Enter).

What to look at

The left cell isn't free (it's body), but it's the tail: Snake lets it through. Without that exception, a four-cell snake couldn't loop around a 2 × 2 square.

The whole lesson on one page

Is it free?

Before adding the new head, Snake asks whether that cell is free. This is isFree, in full:

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);

Off the board, a wall or the body: game over. But there's a subtle exception. The tail is going to move on the same step, so putting the head where the tail is now is fine… as long as you're not eating, because when you eat the tail stays put:

const hitsTail = !eating && head.x === tail.x && head.y === tail.y;

  • x wall
  • c head
  • s body (the top-left one is the tail)
const COLS = 6, ROWS = 5;
const walls = ["xxxxxx", "x....x", "x....x", "x....x", "xxxxxx"].map(f => [...f].map(c => c === "x"));
// head at (3,1), then (3,2), (2,2) and the tail at (2,1): a little square
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("right (4,1):", isFree(4, 1));
console.log("up (3,0), wall:", isFree(3, 0));
console.log("outside (9,9):", isFree(9, 9));
const tail = body[body.length - 1];
const head = { x: 2, y: 1 };   // left: where the tail is
console.log("left (2,1):", isFree(head.x, head.y), "but is it the tail?", head.x === tail.x && head.y === tail.y);

The left cell isn't free (it's body), but it's the tail: Snake lets it through. Without that exception, a four-cell snake couldn't loop around a 2 × 2 square.

Eating: growing and speeding up

Eating means not doing the pop: the head goes in and the tail stays, so the snake is one longer. Also, on that same step:

this.score += 10 + Math.max(0, 8 - this.interval) * 5; // faster = more points this.interval = Math.max(3, 8 - Math.floor(this.eaten / 4)); this.events.push('eat');

Each fruit is worth 10, plus 5 for every tick the snake is already faster than at the start. Going fast is harder, and it pays more. And if there's no free cell left for the new fruit, you've filled the board: the game ends with a 500 bonus.

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("fruit", fruta, "worth", puntos, "| total", score, "| moves every", interval);
  interval = Math.max(3, 8 - Math.floor(eaten / 4));
}

The first four fruits are worth 10; from 21 on, 35 each. Note the order in the real code: points use the interval from before eating, and then it's updated.

Where fruit appears, hunger and the bug

The fruit appears on a random free cell: randomEmpty collects all the free cells in an array and picks one with the day's seeded generator (this.rng.pick(free)). Since the generator is the same on the phone and on the server, the fruit appears in the same place on both.

Two more rules, taken from Nokia and from a review of played games:

  • Hunger: HAMBRE = 30 * 30, 30 seconds' worth of ticks. If you don't eat within that time, you lose. A comment in the code explains that without it, circling a tiny square never ended.
  • Bonus bug: every 5 fruits it appears far from the head and lasts BICHO_DURA = 150 ticks (5 seconds). It's worth 50 + 5 * Math.ceil(vida / 30): 75 if you go for it immediately, down to 55 in the last second. It doesn't make you longer or faster; it's pure score.
// The real generator from 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("free cells:", libres.length);
console.log("phone:", rngTelefono.pick(libres), "server:", rngServidor.pick(libres));

for (const vida of [150, 120, 60, 31, 30, 1])
  console.log("bug with life", vida, "worth", 50 + 5 * Math.ceil(vida / 30));

With the same seed, the phone and the server pick the same cell. Change one of the seeds and they stop matching: that's why the day's seed is decided by the server, not your phone.