Skip to content
Ayotl

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

Letters, sprites and palette

✕
1/7

A 3 × 5 font

Daily Challenge doesn't load a typeface for the score bar: each letter is a drawing 3 pixels wide by 5 tall, stored as five strings of ones and zeros. This is the A in renderer.js:

'A':['010','101','111','101','101']

To write, text() goes through each letter, and for every '1' it paints a 1 × 1 pixel. Then it moves 4 pixels along: 3 for the letter and 1 of space. That's why a text is length × 4 − 1 pixels wide.

leccion.js

$ node leccion.js

Press RUN (or Ctrl+Enter).

What to look at

There's "SNAKE", 5 pixels tall and 19 wide. The whole font has about 60 entries like these; add the T (['111','010','010','010','010']) and write "SKATE".

The whole lesson on one page

A 3 × 5 font

Daily Challenge doesn't load a typeface for the score bar: each letter is a drawing 3 pixels wide by 5 tall, stored as five strings of ones and zeros. This is the A in renderer.js:

'A':['010','101','111','101','101']

To write, text() goes through each letter, and for every '1' it paints a 1 × 1 pixel. Then it moves 4 pixels along: 3 for the letter and 1 of space. That's why a text is length × 4 − 1 pixels wide.

// Five letters from the real font in renderer.js
const FONT = {
  S: ["111", "100", "111", "001", "111"],
  N: ["110", "101", "101", "101", "101"],
  A: ["010", "101", "111", "101", "101"],
  K: ["101", "101", "110", "101", "101"],
  E: ["111", "100", "111", "100", "111"],
};

function text(str) {
  const renglones = ["", "", "", "", ""];
  for (const ch of str) {
    const g = FONT[ch];
    for (let r = 0; r < 5; r++)
      renglones[r] += g[r].replaceAll("1", "#").replaceAll("0", " ") + " ";
  }
  for (const r of renglones) console.log(r);
}

text("SNAKE");
console.log("width in pixels:", "SNAKE".length * 4 - 1);

There's "SNAKE", 5 pixels tall and 19 wide. The whole font has about 60 entries like these; add the T (['111','010','010','010','010']) and write "SKATE".

The bonus bug

Bigger drawings use the same idea. In Snake, every five fruits a bonus bug appears, as in Nokia's Snake II: a 10 × 10 sprite where '1' is a lit pixel and '.' is off. It's 1-bit: it doesn't store colors, only "there or not". The color comes from whoever paints it:

r.sprite(BICHO, x, y, PAL.yellow)

Since it's just text, code can transform it: flip it, count it, compare it.

// The sprite exactly as it is in snake.js
const BICHO = [".1......1.", "..1....1..", "...1111...", "..111111..", ".11.11.11.",
               ".11111111.", "..111111..", ".1.1..1.1.", "1..1..1..1", ".........."];

const pintar = filas => filas.forEach(f => console.log(f.replaceAll("1", "#").replaceAll(".", " ")));
pintar(BICHO);

const encendidos = BICHO.join("").split("").filter(c => c === "1").length;
console.log("lit pixels:", encendidos, "of", BICHO.length * BICHO[0].length);

console.log("--- upside down:");
pintar([...BICHO].reverse());

Antennae, eyes (the gaps in row 5) and legs. The bottom row is empty on purpose: the bug fills a 10 × 10 cell and this way it doesn't touch the one below.

128 colors from one formula

The Atari 2600 had a fixed palette: 16 hues by 8 luminances. core/palette.js reproduces it with a function atari(hue, lum) that returns a CSS color:

  • Hue 0 is the grays: v = lum * 34, from black to almost white.
  • The other hues go around the color wheel backwards, like the console's video chip (the TIA): h = (55 - (hue - 1) * 24 + 720) % 360.
  • More luminance raises the HSL l and lowers the saturation a bit.

Then PAL names the most used ones so nobody has to remember numbers: PAL.green is atari(13, 4).

// The real function from core/palette.js
function atari(hue, lum) {
  hue &= 15; lum &= 7;
  if (hue === 0) { const v = Math.round(lum * 34); return "rgb(" + v + "," + v + "," + v + ")"; }
  const h = (55 - (hue - 1) * 24 + 720) % 360;
  const l = 14 + lum * 10;
  const s = 90 - lum * 5;
  return "hsl(" + h + "," + s + "%," + l + "%)";
}

const PAL = { black: atari(0, 0), white: atari(0, 7), yellow: atari(1, 6),
              red: atari(4, 4), blue: atari(9, 4), green: atari(13, 4), lgreen: atari(13, 6) };
for (const [nombre, color] of Object.entries(PAL)) console.log(nombre.padEnd(7), color);

The snake uses green for the body and lgreen (same hue, lighter) for the head; walls are blue and the fruit red. hue &= 15 clamps any number to 0-15: an out-of-range hue never breaks anything.