Snake on a Cube: 3D Snake in Rust and WebAssembly
Everyone writes Snake at some point — it’s the “hello world” of game loops. So when I came back to it, I wanted the version that quietly breaks your spatial intuition: not Snake on a flat board, but Snake crawling across the surface of a cube, all six faces, running in the browser. snake3d-rs is the result — Rust compiled to WebAssembly, rendered with three-d, and you can play it right now.
The classic part is the easy part
The skeleton of Snake is comfortingly familiar: a snake is a deque of body segments, each tick you push a new head in the current direction and pop the tail (unless you just ate), and the game ends if the head lands on the body. On a flat grid, moving is just arithmetic — bump a coordinate, wrap with a modulo. I had that working in an hour. And then I had to decide what happens at an edge of the cube, and the easy game became an interesting one.
The whole game is the seams
A cube has no single 2D grid. Each face has its own little local coordinate system — call it (u, v) — and the difficulty is entirely in what happens when the snake walks off the edge of one face onto the next. On a flat board you wrap around to the other side. On a cube you cross a seam: you arrive on a different face, at a position that depends on which edge you left, and often facing a different direction than you started.
So the heart of the game is an enum for the six faces, an enum for direction, and a function that — whenever a step would leave the current face — figures out which face you land on, where, and which way you’re now heading:
pub enum Face { Front, Back, Left, Right, Top, Bottom }
pub enum Direction { Up, Down, Left, Right }
fn calculate_next_position(&self, pos: Position, dir: Direction)
-> (Position, Direction) {
// step within the face first...
match dir {
Direction::Up => v += 1, Direction::Down => v -= 1,
Direction::Left => u -= 1, Direction::Right => u += 1,
}
// ...and if that walked us off an edge, hop to the adjacent face:
match (face, dir) {
(Face::Front, Direction::Up) => { face = Face::Top; v = 0; }
(Face::Front, Direction::Right) => { face = Face::Right; u = 0; }
// crossing onto Top can rotate your frame of reference entirely:
(Face::Top, Direction::Right) => { face = Face::Right; u = v; v = n - 1;
new_dir = Direction::Down; }
// ...one careful arm per edge of the cube
}
}
That second match is the entire soul of the project. There are twenty-four of those arms — six faces times four directions — and every single one is a tiny act of origami: I’d hold a paper cube, walk my finger off one face, and write down exactly where it ended up and which way it was pointing. Get one arm’s coordinate swap wrong and the snake teleports or flips, which is both a maddening bug and a weirdly delightful one to watch.
A camera that keeps up
Playing on a cube only works if you can always see where the snake is going, so the camera smoothly rotates to follow the head as it rounds a corner onto a new face, keeping the active face turned toward you. Combined with a semi-transparent voxel board and a glowing grid, the motion sells the illusion that you’re steering something around a solid object rather than shuffling cells in three buffers.
Why Rust and WebAssembly
Two reasons. The first is reach: compiled to WebAssembly, the whole thing runs at near-native speed in any browser with nothing to install — just a link. The second is that the cube’s edge logic is exactly the kind of fiddly, easy-to-get-wrong code where Rust’s enums and exhaustive match earn their keep: the compiler refuses to let me forget a face-and-direction case, which is a real comfort when correctness lives in twenty-four hand-derived seams. Snake turned out to be a perfect little excuse to think in coordinate systems — and a reminder that the oldest games still have new corners to crawl around.
The code is on GitHub at github.com/arazmj/snake3d-rs, and the playable demo is here.