← How a game is made: Daily Challenge's Snake · Lesson 1/16
What we're taking apart
One challenge a day
Daily Challenge puts out a new arcade game every day, Atari 2600 style: one try per person, a board for the day and, at midnight, the top 10. It has 36 games: 29 for the daily challenge and 7 versus games to play with friends.
Snake is one of the shortest and the best known: about 180 lines in a single file, games/snake.js. That's why it's the one we'll take apart. The board is a 16 × 17 grid; the xs in the map are walls, and the snake eats fruit to grow.
The program below reads that map just as it comes in the text file snake_01.txt and counts what's in it.
-
xwall -
sbody -
chead -
ffruit
$ node leccion.js
Press RUN (or Ctrl+Enter).
What to look at
A whole level is an array of strings. The p marks where the snake starts (column 7, row 8). Maps live in .txt files apart from the code: making a new level means drawing, not programming.
The whole lesson on one page
One challenge a day
Daily Challenge puts out a new arcade game every day, Atari 2600 style: one try per person, a board for the day and, at midnight, the top 10. It has 36 games: 29 for the daily challenge and 7 versus games to play with friends.
Snake is one of the shortest and the best known: about 180 lines in a single file, games/snake.js. That's why it's the one we'll take apart. The board is a 16 × 17 grid; the xs in the map are walls, and the snake eats fruit to grow.
The program below reads that map just as it comes in the text file snake_01.txt and counts what's in it.
-
xwall -
sbody -
chead -
ffruit
// The map snake_01.txt, as is (16 columns x 17 rows)
const mapa = [
"xxxxxxxxxxxxxxxx",
"x..............x",
"x..xx......xx..x",
"x..xx......xx..x",
"x..............x",
"x..............x",
"x..............x",
"x..............x",
"x......p.......x",
"x..............x",
"x..............x",
"x..............x",
"x..............x",
"x..xx......xx..x",
"x..xx......xx..x",
"x..............x",
"xxxxxxxxxxxxxxxx",
];
let muros = 0, libres = 0;
for (const renglon of mapa)
for (const c of renglon)
if (c === "x") muros++; else libres++;
console.log("rows:", mapa.length, "columns:", mapa[0].length);
console.log("walls:", muros, "free:", libres);
const y = mapa.findIndex(r => r.includes("p"));
console.log("the snake starts at", { x: mapa[y].indexOf("p"), y });
A whole level is an array of strings. The p marks where the snake starts (column 7, row 8). Maps live in .txt files apart from the code: making a new level means drawing, not programming.
No engine, no build step
There's no Unity, no Phaser and no build step. Each game is a JavaScript module that imports what it needs, and the browser loads it as is:
import { BTN } from '../core/buttons.js';
The strongest reason is a different one: the same file runs on the server. When you finish a game, your phone doesn't send your score: it sends the list of buttons you pressed at each moment. The server (Node.js) imports the very same snake.js, replays the game with those buttons and works out the score itself. If the game depended on an engine that only lives in the browser, that wouldn't be possible.
The program simulates both sides with the same code.
// A tiny "game": scores points when you press A (16)
class Juego {
constructor() { this.score = 0; this.over = false; this.tick = 0; }
update(mask) {
this.tick++;
if (mask & 16) this.score += 10;
if (this.tick >= 6) this.over = true;
}
}
// On the phone: you play and every button is saved
const log = [0, 16, 0, 16, 16, 0];
const telefono = new Juego();
for (const m of log) telefono.update(m);
// On the server: ONLY the log arrives, and it's replayed
const servidor = new Juego();
for (const m of log) servidor.update(m);
console.log("phone:", telefono.score, "server:", servidor.score);
Both give 30. The server never saw the screen or received a point total: it computed it. That idea holds up the whole course, and the server unit tells the full story.
What the language gives you and what it doesn't
The browser comes with almost everything an arcade game needs:
- Canvas 2D to paint pixels.
- Web Audio to generate sound with oscillators, no audio files.
- Keyboard and touch events (
pointerdown,keydown). requestAnimationFrameto draw in step with the screen.- Live connections for online play.
And it has three traps that Daily Challenge's code dodges on purpose:
- A single thread: if a calculation takes long, the screen freezes. So every game step is short.
- Decimal numbers that round:
0.1 + 0.2isn't exactly0.3. For the phone and the server to get the same result, randomness uses 32-bit integers (Math.imul,>>> 0), notMath.random. - The garbage collector: creating new objects every frame causes stutters. Snake empties its event list with
this.events.length = 0instead of making a new one.
console.log(0.1 + 0.2);
console.log(0.1 + 0.2 === 0.3);
// Con enteros de 32 bits no hay redondeo: da igual en todos lados
console.log(Math.imul(123456789, 987654321));
console.log((4000000000 + 500000000) >>> 0);
// Math.random cambia en cada corrida (y no se le puede dar semilla)
console.log(Math.random() === Math.random());
// Vaciar sin crear un arreglo nuevo
const events = ["eat", "die"];
const mismo = events;
events.length = 0;
console.log(mismo, mismo === events);
0.30000000000000004: decimal rounding.Math.imulmultiplies the way C does with 32-bit integers, and>>> 0makes the number unsigned: exact results on any machine.- Emptying with
length = 0keeps the same array (true), no new garbage.