Browser games feel like magic when you’re learning to code. A few lines of JavaScript and shapes move on screen. No compilation. No complex setup. Just open a file and watch it run.

But that magic fades quickly. Spaghetti code piles up. Adding features becomes painful. What started as fun turns into a mess.

I wanted to build a browser game that stayed maintainable. Something with clear structure that I could extend later. Snake seemed perfect. Simple rules, well-understood mechanics, and endless room for polish.

What It Does

The game renders on an HTML Canvas element. You control a cyan snake on a dark gray board. Arrow keys or WASD change direction. The goal is classic: eat the red food, grow longer, avoid your own tail.

The snake starts with four segments and moves at a steady pace. Each food item adds one segment to the body. The game tracks your score in the corner. Hit a wall or your own body, and the game ends.

Press Enter or Space to restart after dying. The simplicity is intentional. I wanted the focus on clean code, not flashy features.

Technical Deep-Dive

Canvas Rendering Architecture

The game uses the Canvas 2D API for all rendering. A single canvas element provides the drawing surface. Each frame, the game clears the background and redraws everything.

Shapes are represented as objects with position, dimensions, and color. The base shape handles drawing rectangles on the canvas. Game elements extend this base to add their specific behaviors.

This inheritance structure keeps drawing logic separate from game logic. The snake doesn’t need to know about canvas operations. It just needs to track its position.

The Game Loop

The heart of the game is a loop that runs every 100 milliseconds. This fixed timestep creates consistent gameplay regardless of browser performance.

Each iteration of the loop follows the same sequence. Draw the background. Move the snake. Check for food collisions. Update the score. Draw all elements. Check for death conditions.

The loop stops when the snake dies. A game over screen appears with instructions to restart. Pressing Enter resets the state and starts fresh.

Snake Movement System

The snake body is an array of segments. Each segment tracks its position and velocity. The head segment leads. All other segments follow.

Movement happens in a chain reaction. First, the head moves based on its current velocity. Then each subsequent segment moves to the previous position of the segment ahead of it. This creates the characteristic snake trail.

Direction changes only affect the head. When you press an arrow key, the head’s velocity updates. The body segments inherit directions naturally as they follow along.

A guard prevents illegal moves. You can’t reverse into yourself. Pressing left while moving right does nothing. This small check prevents frustrating instant deaths.

Collision Detection

The game checks two types of collisions. Wall collisions compare the head position against grid boundaries. Self collisions search the body array for position matches.

The grid uses a coordinate system where each cell is one unit. The canvas pixel position multiplies the grid position by the cell size. This separation makes collision math simpler.

Food uses the same coordinate system. When the head position matches the food position, the snake grows. The food then teleports to a random empty cell.

Food Placement Algorithm

Food needs to appear in unoccupied cells. Spawning inside the snake would be unfair.

The algorithm generates a list of all grid cells. It filters out cells occupied by snake segments. From the remaining cells, it picks one at random. This guarantees valid placement.

For a 20x20 grid with a short snake, the filtering is fast. Performance could matter with very long snakes on larger boards, but this implementation handles typical gameplay well.

Input Handling

Keyboard input maps to game actions through a centralized manager. The manager listens for keydown events and dispatches them to registered callbacks.

Multiple keys can trigger the same action. Arrow keys and WASD both control direction. Enter and Space both restart the game. This flexibility improves the player experience.

The callback pattern allows different game components to respond to input independently. The snake registers direction handlers. The game manager registers restart handlers. Neither needs to know about the other.

The 2021 Refactor

I originally wrote this in JavaScript in 2019. It worked, but the lack of types made changes risky. In 2021, I rewrote everything in TypeScript.

The refactor added type safety throughout. Interfaces define the shapes of game objects. The compiler catches mistakes before runtime. Code navigation in editors improved dramatically.

I also replaced the old build setup with Vite. Hot module replacement speeds up development. The production build is tiny. Deployment to GitHub Pages happens with a single script.

Challenges I Faced

The main challenge was keeping the code organized. Games have many interacting systems. Input, rendering, physics, scoring. Without discipline, these blur together.

I solved this by drawing clear boundaries. Each module has one job. The snake handles movement and growth. The food handles spawning. The game manager coordinates everything.

TypeScript inheritance helped model the relationships. Shapes draw themselves. Game elements are shapes with extra behaviors. This hierarchy emerged naturally from the problem.

What I Learned

This project taught me that browser games don’t need frameworks. The Canvas API is powerful enough for 2D games. Raw JavaScript gives you control over every pixel.

The refactor showed me the value of revisiting old code. What seemed fine in 2019 looked amateur in 2021. Rewriting with more experience produced better results than the original ever could have been.

Simple games make excellent learning projects. The rules are known. The scope is limited. You can focus entirely on how you build, not what you build.

Back to Projects