Summary
I built a neural network that teaches itself to play Snake using only NumPy: the forward pass, loss, backpropagation and optimiser are all written by hand. After 400 games and about 25 seconds of CPU training it averages 32.5 apples in greedy play, with a best training game of 65. It runs live in the browser, costs $0 to host, and lets visitors watch the agent go from random to competent.
Stack
- Training
- Python and NumPy
- Demo
- Plain JavaScript and canvas
- Deployment
- GitHub and a free static host (Netlify)
Problem and goal
Most computer science portfolios carry the same Titanic and house-price notebooks, which show that someone can call a library. I wanted a project that shows I understand how a model actually learns, and that a recruiter can open and understand in under a minute.
The goal was a learning agent built with no ML framework, published as a live link, for $0.
- No frameworkNo PyTorch, TensorFlow or autograd.
- No compute spendTrained on a laptop CPU, no GPU.
- No hosting spendStatic files only, no backend or database.
- No dependencies at runtimeInference is two matrix multiplies in the browser.
Approach
The agent sees 11 yes/no signals instead of raw pixels, and picks one of 3 moves. A tiny state keeps the network small enough to train in seconds on a CPU.
| Component | Design |
|---|---|
| Inputs (11) | Danger ahead, right, left (3)Heading right, down, left, up (4)Food left, right, up, down (4) |
| Actions (3) | Go straight, turn right, turn left |
| Reward | +10 for eating, -10 for crashing or starving (step limit of 100 times the snake length) |
| Network | 11 inputs, 64 hidden neurons (ReLU), 3 outputs, 963 weights |
| Board | 20 by 20 grid |
The network outputs a Q-value for each move, meaning how good that move is expected to be, and the agent plays the highest. NumPy only was a deliberate constraint: writing every gradient by hand forces a real understanding of backprop.
Training
The agent learns with Q-learning: after each move it nudges its prediction for that move toward the reward it got plus the best value it expects next. The error then flows backwards through both layers, which is backprop.
y equals r plus gamma times the max over a prime of Q of s prime, a prime.
The partial derivative of L with respect to Q equals 2 times Q of s, a minus y.
The gradients are written by hand, then updated with a hand-written Adam optimiser.
dq[idx, a] = 2 * (q[idx, a] - y) / ndW2 = h.T @ dqdh = (dq @ W2.T) * (h > 0) # through ReLUdW1 = x.T @ dh| Setting | Value |
|---|---|
| Discount (gamma) | 0.9 |
| Optimiser | Adam, learning rate 0.001 |
| Replay memory | 100,000 transitions |
| Updates | One per move, plus 4 batches of 512 after every game |
| Exploration | Random moves start at 80% and fall to 0% over 90 games |
| Games | 400, about 25 seconds on a CPU |
I kept the method deliberately simple, with no target network, so every moving part can be explained in an interview.
Results
The agent stays near zero while it explores, then jumps once exploration ends: the average goes from 0.6 apples at game 75 to 25.9 by game 125, and settles around 30.
Average score, last 25 games
| Game | 25 | 75 | 100 | 125 | 250 | 400 |
|---|---|---|---|---|---|---|
| Avg score | 0.1 | 0.6 | 8.8 | 25.9 | 31.7 | 30.0 |
The best game during training scored 65. Over 30 greedy games with the final weights (no random moves), the mean was 32.5 and the maximum was 55.
Product and UI
The demo is designed to show learning, not just a final score. It uses a paper-and-ink look: a warm near-black background, cream text, a coral accent, an editorial serif headline and monospace labels, with each panel numbered like a figure in a paper.

- Checkpoint switcher: visitors can play the untrained network, the game 25 and game 80 versions, and the final one, and see the behaviour change.
- Network view (Fig. 2): input neurons light up as the snake senses danger and food, and the three outputs show the live Q-value for each move.
- Training chart (Fig. 3): score per game with a 10-game moving average, next to the best training game, the greedy average over 30 games and the weight count.
- Speed and pause controls: 1×, 2×, 5× and 12× speed, space bar to pause, R to restart.
- How it works notes: three short cards covering the forward pass, the Q-learning target and the hand-written backprop.
- Responsive layout: works on a phone as well as a desktop.
The trained weights are embedded in a single index.html of about 100 KB, so there is no server, no API and nothing to break.
Shipping at $0
Training runs once on a laptop and writes the weights to JSON. A small build script embeds them in the page, so the finished site is plain static files. The code lives in a public GitHub repo, and a free static host (Netlify) deploys it from the main branch with no build command.
| Item | Cost |
|---|---|
| Training compute | $0 (laptop CPU) |
| Hosting | $0 (free static hosting) |
| Domain | $0 (default subdomain) |
| Backend and database | None needed |
What I learned and what's next
What I learned
- Replay memory is what makes learning stable: training on one move at a time alone was too noisy.
- Checkpoints tell the story better than a final score, so the demo lets visitors replay each stage.
- A small, well-chosen state (11 signals) can beat a complex one when compute is limited.
What's next
- Add a target network and compare results across several seeds.
- Add features that see the snake's own body, such as free space and distance to the tail, to break the plateau near 30.
- Let visitors train the network live in the browser.
- Add a simple rule-based baseline to show how much the network adds.
Watch it go from random to competent
Switch checkpoints in the live demo, or read the code on GitHub.


