· 2 MIN READ
Shipping a Game in a Language With No Exceptions
Postmortem of GhostBust Hotline: what it's like to lead a three-person team building a real-time shooter in Elm — a pure functional language — and why the compiler became our best teammate.
- #games
- #elm
- #functional-programming
- #postmortem
This summer, three of us shipped GhostBust Hotline — a 2D top-down shooter where the ghosts are invisible until you reveal them. The unusual part isn't the game. It's that every line of it is written in Elm, a pure functional language with no runtime exceptions, no mutable state, and absolutely no sympathy for the way game programmers usually think.
You can't just enemy.hp -= damage
In most engines, gameplay code is a pile of objects mutating each other.
Elm doesn't have that. The entire game is one immutable state value, and the
only way anything happens is a pure function: update : Msg -> Model -> Model.
A ghost taking damage isn't a method call — it's a message routed to the ghost, producing a new ghost, producing a new world. At first this feels like moving furniture while wearing oven mitts. Around week three it inverts: nothing ever changes for a reason you can't trace. Every bug is reproducible, because the same message sequence always produces the same state. Our worst debugging session lasted about twenty minutes. I've had Unity sessions that lasted days.
Particles without a particle engine
Elm has no particle libraries worth using, so the muzzle flashes, ghost reveals, and death bursts are hand-rolled: each particle system is a list of records folded forward each frame.
updateParticle : Float -> Particle -> Maybe Particle
updateParticle dt p =
if p.life <= 0 then
Nothing
else
Just
{ p
| pos = add p.pos (scale dt p.vel)
, vel = scale (1 - p.drag * dt) p.vel
, life = p.life - dt
}
Tuning these frame by frame was slow — and completely worth it. Hand-tuned particles are to a game what good typography is to a website: nobody notices them, everybody feels them.
The AI design twist
Invisible enemies break the usual AI contract, where the player reads enemy animation to plan. Our fix: each ghost type telegraphs through the environment instead — distinct movement noise, distinct particle disturbances — so revealing a ghost is a skill, and each type demands a different strategy once visible. Per-ghost AI behaviours in a pure language turn out to be pleasant: each behaviour is just a function from world-state to intent, trivially testable in isolation.
Leading a three-person team
Two process decisions paid for themselves:
- CI from day one. GitHub Actions verifying Elm compilation on every push. In Elm, "it compiles" is an absurdly strong guarantee, so green CI meant the build was always playable. Demo anxiety: zero.
- Vertical slices, not layers. Nobody owned "the rendering layer." Each of us owned features end to end, which is the only split that works in a codebase where everything flows through one update function.
I also cut the trailer in DaVinci Resolve, which taught me that marketing a game is its own craft and I respect it now.
Verdict
Would I build another game in Elm? For a jam or a course — instantly. The compiler catches entire categories of bugs before they exist, and the architecture scales down to small teams beautifully. You can play GhostBust Hotline here.