Skip to content
Ayotl

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

The body is a queue

✕
1/8

A list of cells

The snake isn't a picture: it's an array of cells { x, y }, with the head at position 0. It starts three long. The constructor in snake.js picks the direction with the most free path from the start and puts the body behind it, the opposite way:

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 }];

Drawing it means walking the array: cell 0 in light green and the rest in regular green.

leccion.js

$ node leccion.js

Press RUN (or Ctrl+Enter).

What to look at

The head (C) in column 4 and the body to its left, because it faces right. Change dir0 to { x: 0, y: 1 } (down) and the body ends up above the head.

The whole lesson on one page

A list of cells

The snake isn't a picture: it's an array of cells { x, y }, with the head at position 0. It starts three long. The constructor in snake.js picks the direction with the most free path from the start and puts the body behind it, the opposite way:

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 }];

Drawing it means walking the array: cell 0 in light green and the rest in regular green.

const COLS = 10, ROWS = 5;
const start = { x: 4, y: 2 }, dir0 = { x: 1, y: 0 };   // facing right
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);

The head (C) in column 4 and the body to its left, because it faces right. Change dir0 to { x: 0, y: 1 } (down) and the body ends up above the head.

Moving: in at the front, out at the back

To move forward, not every piece moves. The new head cell is computed, added at the start with unshift, and the last one is removed with pop. Everything in between stays where it was, and yet the snake looks like it's slithering.

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

It's a double-ended queue (deque): items go in one end and out the other. In an array, unshift costs O(n) because it shifts everything to make room, but here n never exceeds 16 × 17 = 272 cells: you can't tell.

(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("start   ", pintar());
for (const dir of pasos) {
  const head = { x: body[0].x + dir.x, y: body[0].y + dir.y };
  body.unshift(head);   // the head goes in
  body.pop();           // the tail comes off
  console.log("step    ", pintar());
}
console.log("length:", body.length);

Each output line is the whole board flattened (its five rows one after another). The snake moves on, goes down twice and turns left, and the length stays 3: for every head that goes in, a tail comes off.

Never in reverse

The controls are read every tick, but the snake only moves every interval ticks. So there are two directions: dir, the one it's going, and nextDir, the one you asked for on the next step.

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 }; …

Each condition forbids the opposite: if you're going down (dir.y === 1), up is ignored. And it compares against dir, the real direction, not the requested one. If you're going right and within one step you press up and then left, left is ignored: otherwise the head would run into its own neck.

const BTN = { UP: 1, DOWN: 2, LEFT: 4, RIGHT: 8 };
const s = { dir: { x: 1, y: 0 }, nextDir: { x: 1, y: 0 } };   // going right

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("left while going right:", s.nextDir);
leer(BTN.UP);
console.log("up:", s.nextDir);
leer(BTN.LEFT);
console.log("left before moving:", s.nextDir);
s.dir = s.nextDir;   // the tick where it moves arrives
leer(BTN.LEFT);
console.log("left once going up:", s.nextDir);

The first left is ignored (it would be reversing). Up is accepted. The second left is also ignored, because dir is still "right" until the snake moves. Once it's moving up, left is accepted.