The console ◆ 3-D Snake ◆ 3-D Pong ◆ 512 LEDs ◆ MIE438
Two games written for a display that is 8 cells wide, 8 deep and 8 tall, and that can only ever light one layer at a time. Snake gains a third axis and loses its walls — run off one face and you come back on the other — and steers with four keys that turn it relative to itself, not six that name world axes. Pong gains a machine opponent that solves for where the ball will cross its own face and moves to meet it. Both run on the same multiplexed frame buffer the hardware uses.
Drag the cube to turn it — you will want to, because the snake occludes itself from every fixed angle. Scores are kept in your own browser and nowhere else.
Two things change when Snake moves into a volume, and only one of them is the obvious one.
The obvious one is the input space. Six axes need six buttons, and two of them always point along the direction you are looking down — so they are the two you cannot see yourself moving in. That is what the PS2 pad's shoulder buttons were for on the hardware, and it is genuinely hard to play, because every keypress has to be translated out of the world's frame and into the one you are looking at.
So the version here steers relative to the snake instead. It carries its own frame — a forward vector and an up vector, with left and right falling out of their cross product — and the four arrow keys are turns, not axes:
Four keys instead of six, nothing to translate, and it stays on the integer lattice because every vector involved is a unit axis. Two properties come free and are worth having. Four presses of the same key return the snake exactly to where it started, so the controls are a group you can feel out rather than memorise. And no key can ever reverse the snake — a turn is always perpendicular to travel — so unlike the absolute scheme, no single press can kill you. (Verified over 20,000 random turns: the frame stays orthonormal and integer, and there is not one reversal.)
The one that actually changes the game is density. A 2-D 8 × 8 board is 64 cells and a snake of length 20 fills nearly a third of it; the 8 × 8 × 8 cube is 512, and the same snake fills 4 %. Self-collision stops being the pressure it is in two dimensions, and the difficulty moves entirely into navigation — finding a cell you cannot see because your own body is between it and your eye.
So the walls come off. Running off the +x face puts you back on −x, which removes the only remaining source of cheap deaths and leaves the game to be about the thing that is hard:
The double modulo is not decoration; JavaScript's % keeps the sign of its left operand, so -1 % 8 is -1, and a single modulo sends the snake to index −1 rather than to the far face.
The rest matches the hardware version: no straight reversal (the one input the PS2 build also refuses, because it is always a self-collision), grow by one on food, and food respawns only into a cell the body does not occupy.
On a binary display the head and the body are the same brightness, so at any moment you cannot tell which end of a 20-cell curve you are steering. The fix that costs nothing is temporal: the head is toggled on and off at about 2 Hz, which is far below the refresh rate and therefore free — the multiplexer was going to redraw the frame anyway. It is the only per-LED "brightness" this display has, and it is spent on the one bit of state the player genuinely cannot infer.
The Pong opponent does not chase the ball. Chasing looks plausible and plays badly: the paddle is always behind, and against a fast ball it never arrives. Instead it does what a person does, which is to solve for where the ball will be when it gets there.
The ball travels in a straight line between bounces, so the time to reach the machine's face is exact:
and the free-flight crossing point is x + vxt*, z + vzt*. The four side walls make that a guess rather than an answer, and the fix is to fold the coordinate rather than iterate the bounces — reflect it back into the box with a triangle wave of period 2(N−1), which is exact for any number of wall hits and costs one modulo:
Then the paddle moves toward that point at a capped rate. That cap is the only thing that makes the machine beatable: the prediction itself is exact, so if the paddle could teleport there would be no game.
The first version only defended, and against a competent player it produced rallies that never ended — both paddles arrived in time, every time, for minutes. A predictor that only defends cannot win; it can only fail to lose.
So the machine aims. Contact away from the paddle centre already imparts lateral velocity to the ball — that is what makes your own shots steerable — so the machine deliberately places its paddle off the intercept, on the side that throws the ball away from where your paddle currently is:
The offset has to stay inside the paddle's 1.5-cell half-width or the machine misses its own shot, which is a pleasing constraint: it can only aim as hard as it can still catch.
Skill does not degrade the prediction. A perfect predictor that merely moves slowly is beatable but never wrong, and reads as a machine asleep rather than one you out-thought. Skill instead moves three things at once: paddle rate 2.6 → 8.8 cells/s, aim error ±1.4 → 0 cells, and aim offset 0 → 0.9 cells. At the bottom it misjudges by most of a paddle width and you can pick a corner; at the top it is exact, and it is trying to make you run.
It is honest about the one thing it cannot do: while vy ≤ 0 the ball is travelling away and there is nothing to predict, so it holds its last intercept — the same hedge a human plays.
| Slider | Paddle | Aim error | You win |
|---|---|---|---|
| Sleepy | 2.6 cells/s | ±1.4 | 97 % |
| Easy | 4.2 | ±1.05 | 70 % |
| Fair | 5.7 | ±0.70 | 51 % |
| Sharp | 7.3 | ±0.35 | 28 % |
| Ruthless | 8.8 | 0 | 0 % |
Share of points won, over 3 × 200 s per setting against a simulated player: one that predicts the intercept the same way the machine does, but on a 200 ms stale view of the ball and with ±1.2 cells of aim error, moving at 10 cells/s. That stand-in is a modelling choice, not a measurement of you — it exists so the slider could be calibrated to be monotone and to sit near even at the midpoint, which it does. Where you actually fall on it is the interesting part.
The paddle is 3 × 3 on an 8 × 8 face, so it covers 14 % of what it defends, and the ball speeds up 4 % per exchange to a cap of 18 cells/s — without that cap it eventually moves more than a cell per frame and passes through a paddle instead of hitting it.
MIE438: Microprocessors and Embedded Microcontrollers at the University of Toronto, joint work with Jiaming (Jason) Dong. The cube, the PCB design, the display library and both games are from the course project.
The browser console on this page — the frame buffer, the multiplexer, the persistence-of-vision model, wrap-around Snake and the Pong opponent — was written afterwards, so the games can be played rather than described.