← How a game is made: Daily Challenge's Snake · Lesson 6/16
One bitmask per tick
Five buttons, five bits
The Atari had a four-way joystick and one button. Daily Challenge stores all five in a single number, one bit per button. That's all of core/buttons.js:
export const BTN = { UP: 1, DOWN: 2, LEFT: 4, RIGHT: 8, A: 16 };
Each value is a power of 2, so they can be combined without clashing: up and right together is 1 | 8 = 9. To ask whether a button is pressed you use bitwise and: mask & BTN.UP is non-zero if the up bit is on. Five bits give 32 combinations: from 0 (nothing) to 31 (everything).
$ node leccion.js
Press RUN (or Ctrl+Enter).
What to look at
The middle column is the number in binary: each 1 is a pressed button. 25 is 11001: A, right and up.
The whole lesson on one page
Five buttons, five bits
The Atari had a four-way joystick and one button. Daily Challenge stores all five in a single number, one bit per button. That's all of core/buttons.js:
export const BTN = { UP: 1, DOWN: 2, LEFT: 4, RIGHT: 8, A: 16 };
Each value is a power of 2, so they can be combined without clashing: up and right together is 1 | 8 = 9. To ask whether a button is pressed you use bitwise and: mask & BTN.UP is non-zero if the up bit is on. Five bits give 32 combinations: from 0 (nothing) to 31 (everything).
const BTN = { UP: 1, DOWN: 2, LEFT: 4, RIGHT: 8, A: 16 };
function nombres(mask) {
const lista = Object.keys(BTN).filter(b => mask & BTN[b]);
return lista.length ? lista.join("+") : "nothing";
}
for (const m of [0, 1, 9, 16, 25, 31]) {
console.log(String(m).padStart(2), m.toString(2).padStart(5, "0"), nombres(m));
}
console.log("up + right =", BTN.UP | BTN.RIGHT);
The middle column is the number in binary: each 1 is a pressed button. 25 is 11001: A, right and up.
Holding and tapping: held and latched
core/input.js merges three sources into the same bitmask: the keyboard (arrows, WASD, space), the on-screen D-pad and swipes over the game. It keeps them in two places:
held: what's pressed right now. A key turns its bit on when it goes down and off when it comes up.latched: what happened just once, like a swipe. It's kept until the next tick and then cleared, so a lightning-fast swipe between two ticks isn't lost.
Once per tick, the loop calls poll():
const m = this.held | this.latched; this.latched = 0; return m;
const BTN = { UP: 1, DOWN: 2, LEFT: 4, RIGHT: 8, A: 16 };
const input = {
held: 0, latched: 0,
bajaTecla(b) { this.held |= b; },
subeTecla(b) { this.held &= ~b; },
swipe(b) { this.latched |= b; },
poll() { const m = this.held | this.latched; this.latched = 0; return m; },
};
const log = [];
input.bajaTecla(BTN.RIGHT); log.push(input.poll()); // tick 1
log.push(input.poll()); // tick 2: still held
input.swipe(BTN.UP); log.push(input.poll()); // tick 3: the swipe
log.push(input.poll()); // tick 4: already cleared
input.subeTecla(BTN.RIGHT); log.push(input.poll()); // tick 5
console.log(log);
[8, 8, 9, 8, 0]: right stays on while the key is down; the upward swipe shows up exactly once (8 + 1 = 9) and goes away. ~b flips the bits: held &= ~b turns off just that button.
Pressing isn't holding
Snake only needs to know what's held: if (mask & BTN.UP && this.dir.y !== 1) …. But other games need the moment a button goes down: in Tetris, holding the button shouldn't rotate the piece 30 times a second. For that, buttons.js has a second function:
export const pressed = (mask, prev) => mask & ~prev;
It returns the bits that are on now but weren't on the previous tick: the edges.
const pressed = (mask, prev) => mask & ~prev;
// A held for three ticks, released and pressed again
const masks = [0, 16, 16, 16, 0, 16];
let prev = 0, giros = 0;
for (const m of masks) {
if (pressed(m, prev) & 16) giros++;
prev = m;
}
console.log("ticks with A:", masks.filter(m => m & 16).length, "| rotations:", giros);
Four ticks with A down, but only two rotations: one for each time the button went down.
The whole game, as text
Every tick, main.js saves the bitmask before handing it to the game: log.push(mask); game.update(mask);. At the end, the whole log is an array of numbers from 0 to 31, one per tick.
To send it to the server, net.js turns each number into a character by adding 48 (the code of '0'):
log.map(m => String.fromCharCode(48 + m)).join('')
A one-minute game is 1800 characters. The server does the reverse with charCodeAt(0) - 48 and rejects the log if any number falls outside 0 to 31.
const log = [0, 0, 8, 8, 9, 1, 1, 16, 0];
const texto = log.map(m => String.fromCharCode(48 + m)).join("");
console.log("for the server:", texto);
// on the server's side
const deVuelta = Array.from(texto, ch => ch.charCodeAt(0) - 48);
console.log("read back:", deVuelta);
console.log("valid?", deVuelta.every(m => m >= 0 && m <= 31));
console.log("one minute =", 30 * 60, "characters");
0 to 9 come out as digits, 10 to 15 as symbols like : and ?, 16 is @ and 17 to 31 are the letters A to O. One character per tick: compact and easy to check.