All projects

Functional Programming · BSc coursework

Functional Tetris

Tetris with no mutable state anywhere. Every tick, rotation and drop is a pure function from one game state to the next — which turns rotation, the awkward part of Tetris, into a geometry problem rather than a bookkeeping one.

Role
Game engine — state model, rotation system, collision logic
Context
Functional Programming coursework, VU Amsterdam
Constraint
No mutable containers, no mutable local variables
Stack
  • Scala
  • Immutable data structures
  • Higher-order functions
  • Inheritance

The constraint

Games are the canonical example of state that changes constantly, and the canonical implementation is a mutable grid updated in place. The brief here forbids that: no mutable collections, no reassignable variables. Every transition has to produce a new game state rather than edit the existing one.

That sounds like an obstacle and mostly turns out to be a simplification. A state transition that cannot touch anything outside itself is testable in isolation: feed it a board, check the board that comes back. There is no hidden state to set up first and none left behind afterwards.

Representing pieces

A tetromino is a set of points relative to an anchor, not a fixed pattern stamped into grid coordinates. Position is the anchor; shape is the offsets. That separation is what makes rotation tractable, because rotating becomes a transformation of the offsets around the anchor, independent of where the piece sits on the board.

Rotation, and why it is the hard part

Five of the seven tetrominoes — J, L, S, T and Z — rotate cleanly: 90-degree pivots about a centre block that genuinely exists in the piece. The other two do not, and they are why naive Tetris implementations feel subtly wrong:

  • The O piece is a 2×2 square. It has no centre block and rotating it changes nothing, so its rotation must be an explicit no-op rather than a transformation that happens to land back where it started.
  • The I piece is four in a row. Its centre of rotation falls between cells, not on one, so the standard pivot pushes it off by a cell in one direction or the other.

Anchor-relative representation is what resolves this. Because the anchor is a property of the piece rather than a fixed grid cell, the I and O pieces can define anchors that make the same rotation formula produce correct results for them too — instead of special-casing them inside the rotation code.

Structurally this is an abstract tetromino type with subclasses supplying the three distinct rotation behaviours. It is a deliberately object-oriented seam inside an otherwise functional design, and it is the right one: the variation is in behaviour per piece type, which is exactly what subtyping expresses well.

Collision and hard drop

Collision detection is a predicate over the candidate piece's occupied cells: does any of them fall outside the board or onto a filled cell? Expressed with exists over the offsets, it is a single expression, and every movement operation — left, right, rotate, tick down — becomes "compute the candidate, accept it if it is legal, otherwise keep the current state".

Hard drop reuses that same predicate: find the furthest downward translation for which the piece is still legal, and move there in one step. No loop with a mutable counter — just a search over candidate positions filtered by the collision predicate.

Why it was worth doing

Writing it this way forces every rule of the game to be stated explicitly as a function, rather than emerging from the order in which a loop happens to mutate things. Line clearing, collision, rotation and scoring each become a transformation you can point at, test and reason about on its own.

Want the detail?

Happy to talk through the rotation system or the state model.

hendrikjoel21@gmail.com