← How a game is made: Daily Challenge's Snake · Lesson 4/16
Letters, sprites and palette
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.
$ 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
0is 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
land 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.