๐ŸŸจ My First Browser Game ยท Episode 4 of 6 ยท See All Episodes
๐Ÿ Episode 4 ยท Improver ยท Grid Logic

Pixel Hunter!
Browser Snake

Snake on canvas with crisp grid movement, an input queue that fixes THE classic Snake bug, wrap-around walls as an option, and touch controls so it plays on a phone.

๐Ÿ‘ถ Ages 10+ โฑ๏ธ ~1.5 Hours ๐ŸŸจ JavaScript โœ“ Free
๐Ÿ”ฒ Grid ticks ๐Ÿ“ฅ Input queue ๐ŸŽ Food logic ๐ŸŒ€ Wrap-around ๐Ÿ“ฑ Touch controls ๐Ÿ… High score
โญ
0 XP
Level 1
๐Ÿ”ฅ0
Your Progress 0 / 6 steps
1
๐Ÿ”ฒ
Grid & Tick Loop
A 25ร—25 grid stepped every 120 ms
Active
๐ŸŽฏ
Goal for this step

Set up grid-based movement on a fixed tick inside rAF.

  • 1500ร—500 canvas = 25ร—25 cells of 20 px. The snake: an array of {x, y} cells, head first.
  • 2Tick inside requestAnimationFrame: accumulate timestamps; every 120 ms take ONE grid step.
  • 3rAF passes a timestamp, use it: if (t - lastTick > 120).
  • 4Move = unshift new head + pop tail (the deque trick from C++ Snake, JS edition: unshift/pop).
snake.html
let snake = [{x:12,y:12},{x:11,y:12},{x:10,y:12}];
let dir = {x:1, y:0};
let lastTick = 0;

function loop(t) {
    if (t - lastTick > 120) {
        lastTick = t;
        const head = { x: snake[0].x + dir.x,
                       y: snake[0].y + dir.y };
        snake.unshift(head);
        snake.pop();
    }
    draw();
    requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
โœ๏ธ
Fill in the Blanks
+15 XP
JavaScript arrays grow at the front with and shrink at the back with . One step happens every milliseconds.
๐Ÿง 
Knowledge Check
+15 XP
Drawing happens every rAF frame but movement only on ticks. Why split them?
ATo confuse readers
BSmooth 60 FPS rendering with classic chunky Snake movement, each system at its right rate
CrAF requires it
2
๐Ÿ“ฅ
The Input Queue
Fix Snakeโ€™s most famous bug properly
Locked
๐ŸŽฏ
Goal for this step

Queue direction changes so fast double-presses cannot reverse the snake.

  • 1The bug: pressing up-then-left inside one tick lets the second press reverse you into your neck.
  • 2The pro fix: push wanted directions into a QUEUE; each tick, shift ONE off and validate it against the current direction.
  • 3Validation: reject exact reversals (new.x === -dir.x && new.y === -dir.y).
  • 4Queued input also FEELS better, fast corner combos come out exactly as typed. This is how real Snake implementations do it.
snake.html
let inputQueue = [];

document.addEventListener("keydown", function(e) {
    const map = { ArrowUp:{x:0,y:-1}, ArrowDown:{x:0,y:1},
                  ArrowLeft:{x:-1,y:0}, ArrowRight:{x:1,y:0} };
    if (map[e.key]) inputQueue.push(map[e.key]);
});

// at each tick, before moving:
while (inputQueue.length) {
    const want = inputQueue.shift();
    if (want.x !== -dir.x || want.y !== -dir.y) {
        dir = want;
        break;                      // one turn per tick
    }
}
โŒจ๏ธ
Code Challenge
+20 XP
Take one valid direction per tick from the queue:
snake.html
const want = inputQueue.();
if (want.x !==  || want.y !== -dir.y) {
    dir = want;
    ;
}
๐Ÿ’ก Hint: Remove from the queue front; reject the exact opposite of the current direction; stop after one accepted turn.
๐Ÿง 
Knowledge Check
+15 XP
Why is a queue better than the "nextDir" single-buffer fix (C++ episode)?
AQueues are newer
BFast combos (up-left within one tick) are BOTH remembered and applied over two ticks instead of one being lost
CArrays are cheaper
3
๐ŸŽ
Food, Growth & Score
Spawn-avoid-the-snake plus eat-to-grow
Locked
๐ŸŽฏ
Goal for this step

Add food that respawns safely and grows the snake.

  • 1Food = one {x, y}. Eating: new head equals food โ†’ skip the pop (grow) and respawn.
  • 2Safe respawn: loop random cells until none of the snake matches (Array.some!).
  • 3Score = snake.length โˆ’ 3, drawn in the corner.
  • 4Draw food as a pulsing circle, scale its radius with Math.sin(t / 150) for a free animation.
snake.html
function spawnFood() {
    while (true) {
        const f = { x: Math.floor(Math.random() * 25),
                    y: Math.floor(Math.random() * 25) };
        const onSnake = snake.some(c => c.x === f.x && c.y === f.y);
        if (!onSnake) return f;
    }
}
let food = spawnFood();

// in the tick:
snake.unshift(head);
if (head.x === food.x && head.y === food.y)
    food = spawnFood();          // grew, no pop
else
    snake.pop();
โœ๏ธ
Fill in the Blanks
+15 XP
The safe-spawn check uses the array method , which asks if ANY cell matches. Growth happens by skipping the .
๐Ÿง 
Knowledge Check
+15 XP
snake.some(c => c.x === f.x && c.y === f.y) returnsโ€ฆ
AThe matching cell
Btrue if at least one body cell is at the food position, the "is it on the snake?" answer
CA new array
4
๐ŸŒ€
Death, or Wrap-Around
Classic walls vs Pac-Man edges, as a toggle
Locked
๐ŸŽฏ
Goal for this step

Implement both wall modes and self-collision death.

  • 1Classic mode: head outside 0-24 โ†’ dead. Wrap mode: teleport across, (head.x + 25) % 25 handles both edges in one line!
  • 2A checkbox above the canvas toggles the mode.
  • 3Self-bite: head matches any body cell (some again) โ†’ dead.
  • 4Death: stop ticking, dim the board, show score, Space restarts. Wrap mode is friendlier, mode choices widen your audience.
snake.html
const wrapMode = document.getElementById("wrap").checked;

if (wrapMode) {
    head.x = (head.x + 25) % 25;
    head.y = (head.y + 25) % 25;
} else if (head.x < 0 || head.x > 24 ||
           head.y < 0 || head.y > 24) {
    dead = true;
}

if (snake.some(c => c.x === head.x && c.y === head.y))
    dead = true;
โŒจ๏ธ
Code Challenge
+20 XP
Wrap the head across the edges in one line per axis:
snake.html
head.x = (head.x + ) % ;
๐Ÿ’ก Hint: Adding the grid size first makes -1 wrap to 24; the remainder brings 25 back to 0.
๐Ÿง 
Knowledge Check
+15 XP
Why add 25 before taking % 25?
ABecause % is broken
BJavaScriptโ€™s % keeps the sign: -1 % 25 is -1, but (-1 + 25) % 25 is the desired 24
CTo slow the snake
5
๐Ÿ“ฑ
Touch Controls
Swipe detection makes it a phone game
Locked
๐ŸŽฏ
Goal for this step

Add swipe input so the game works on any phone.

  • 1Record the touch start point (touchstart); on touchend compare positions.
  • 2The bigger delta axis wins: |dx| > |dy| โ†’ left/right swipe, else up/down. Push into the SAME inputQueue, new input, zero new game logic.
  • 3Add e.preventDefault() and the touch-action CSS so the page does not scroll while swiping.
  • 4Open it on your phone (same Wi-Fi + a local server, or just send the file). Mobile game: done.
snake.html
let touchStart = null;
canvas.addEventListener("touchstart", function(e) {
    touchStart = { x: e.touches[0].clientX,
                   y: e.touches[0].clientY };
    e.preventDefault();
});
canvas.addEventListener("touchend", function(e) {
    if (!touchStart) return;
    const dx = e.changedTouches[0].clientX - touchStart.x;
    const dy = e.changedTouches[0].clientY - touchStart.y;
    if (Math.abs(dx) > Math.abs(dy))
        inputQueue.push({ x: dx > 0 ? 1 : -1, y: 0 });
    else
        inputQueue.push({ x: 0, y: dy > 0 ? 1 : -1 });
});
โœ๏ธ
Fill in the Blanks
+15 XP
A swipeโ€™s direction comes from comparing (dx) against abs(dy). Touch input pushes into the same input as the keyboard, no new game logic.
๐Ÿง 
Knowledge Check
+15 XP
Feeding touch into the existing inputQueue demonstratesโ€ฆ
ALazy coding
BGood architecture: input SOURCES vary, the game logic stays one clean system
CA browser quirk
6
๐Ÿ…
Speed, Score & Polish
Faster ticks, gradient body, saved best
Locked
๐ŸŽฏ
Goal for this step

Finish with a speed curve, a pretty snake and a persistent record.

  • 1Tick time shrinks with length: Math.max(60, 120 โˆ’ snake.length * 2).
  • 2Gradient body: colour each segment by its index (HSL makes this one line, hsl(140, 70%, L%) with L fading tailward).
  • 3Best score in localStorage (Episode 1 skill, same three lines).
  • 4Four browser games down. The finale upgrades your platformer to a full game!
snake.html
const tickTime = Math.max(60, 120 - snake.length * 2);

// gradient body while drawing (i = segment index):
snake.forEach(function(c, i) {
    const light = 55 - Math.min(35, i * 1.5);
    ctx.fillStyle = "hsl(155, 70%, " + light + "%)";
    ctx.fillRect(c.x * 20 + 1, c.y * 20 + 1, 18, 18);
});
โŒจ๏ธ
Code Challenge
+20 XP
Speed up with length, with a floor:
snake.html
const tickTime = Math.(60, 120 - snake. * 2);
๐Ÿ’ก Hint: The floor function picks the larger of the minimum (60) and the shrinking value based on how long the snake is.
๐Ÿง 
Knowledge Check
+15 XP
HSL colour ("hsl(155, 70%, 40%)") beats hex codes for the gradient body becauseโ€ฆ
AIt is shorter to type
BYou can compute ONE number (lightness) per segment, hex would need string maths on three channels
CBrowsers render it faster
๐ŸŽ‰๐Ÿ†๐ŸŽฎโœจ๐ŸŽ‰
Workshop Complete!
Grid ticks, input queues and touch support, Snake that plays great on desktop AND phone. The finale awaits: Platformer Part 2!
0
Total XP
1
Level
0
Best Streak
0%
Accuracy
โ–ถ Episode 6: Platformer Part 2 โ†’
โญ View My Progress & Certificates

This workshop was free and took many hours to build. If it helped you learn something new, consider supporting the project.

☕ Support on Ko-fi