All projects

Functional Programming · BSc coursework

Functional Snake

Snake with immutable state throughout — which makes the board cheaper to represent, the randomness reproducible, and rewinding time almost a side effect of the architecture rather than a feature that had to be built.

Role
Game engine — state model, movement, apple placement, rewind
Context
Functional Programming coursework, VU Amsterdam
Constraint
No 2D arrays, no mutable variables
Stack
  • Scala
  • Persistent collections
  • Recursion
  • Pattern matching

Representing the board

The instinctive representation of a Snake board is a 2D grid of cells, each marked empty, snake or apple. It is also the wrong one: the overwhelming majority of those cells are empty, and every tick iterates over all of them to find the few that are not.

Instead the snake is a list of points and the apple is a single point. Nothing represents empty space at all, because empty space is simply the absence of an entry. Movement becomes a list operation — prepend the new head, drop the tail — and self-collision becomes a membership check against the body.

The grid version stores mostly nothing and spends most of its time looking at it. The list version stores exactly the state that exists.

Wrap-around boundaries

Reaching an edge does not end the game — the snake re-emerges on the opposite side. With coordinates rather than a grid this is arithmetic on the head position rather than a special case in the collision logic, so the boundary rule stays in one place instead of being scattered through movement handling.

Reproducible randomness

Random apple placement and deterministic testing are usually in tension: a test cannot assert where the apple lands if a fresh random number decides it each run.

The resolution is to make placement a pure function of a single number. Every free cell on the board is enumerated in a fixed order, and one random integer selects an index into that enumeration. Given the same board and the same integer, the apple always lands in the same place — so the game is genuinely random in play and completely reproducible under test, without a separate test-only code path.

Deferred growth

Eating an apple does not lengthen the snake instantly; it extends over the following steps, so growth is visible as it happens rather than appearing in a single frame. That means a tick has to carry a small amount of pending state — how much growth is still owed — which the state object holds explicitly instead of tracking through a mutable counter somewhere on the side.

Rewind

The feature that justifies the whole approach. Because every tick produces a new immutable game state rather than editing the previous one, the earlier states still exist and are still valid. Keeping them in a stack means rewinding is nothing more than popping back to an earlier one — and releasing the rewind key resumes normal play from that point, since a past state is as playable as the present one.

In a mutable implementation this is a substantial feature: you would need to serialise snapshots, or record and invert every operation. Here it is a consequence of already having refused to overwrite anything.

Why it was worth doing

Undo, replay and time travel all look like advanced features and are mostly a property of how state is stored. Building the game without mutation makes that concrete — the expensive-sounding feature turned out to be the cheap one, because the architecture had already paid for it.

Want the detail?

Happy to talk through the state model or the rewind mechanism.

hendrikjoel21@gmail.com