Writing a snake clone in Haskell, part 1

Thu 16 November 2017
tags: haskell

After my recent dive into Haskell I was keen to try a small project to test out what I had learned. After watching a bunch of YouTube videos from various Haskell conferences I came across one by Moss Collum where he describes how he built a series of Rogue-like games in Haskell over the course of a week.

I took a look at Moss' code and thought it was a pretty neat idea, however I wanted to try and make a snake game instead (nostalgia for the Nokia 3310, I guess). If you don't know what snake is, here's a sweet GIF of a russian guy getting a perfect game:

snake

You move a snake around the screen trying to gobble up pieces of food. The snake moves forward 1 space every second or so autonomously, and each piece of food you eat makes the snake grow 1 space longer. You die if the snake hits the walls or its own tail.

The snake games that I saw on Hackage all seemed to be projects for the author to learn how to use a specific library, and I found that as a consequence the code logic was somewhat obscured. I wanted something much simpler: a terminal application controlled by sending keypresses to stdin, and with ASCII "graphics". I specifically wanted to avoid using game libraries; after all, my aim was to exercise my Haskell knowledge, not to make a novel gaming experience!

Let's go

All of the code described here is available on Github.

First iteration

I started out by looking at some of Moss' code to see an example of how I could proceed. I decided that the first thing I would do would be to have a snake of fixed length moving around the screen in response to keypresses: no "food" to grow the snake, no boundaries, no collision detection and (most importantly) the snake does not move by itself.

The best way of proceeding seemed to be to model the game as a sequence of transformations on an initial state of the game's world. The transformations to apply are determined by the commands typed by the player. Moss' code took advantage of Haskell's lazy IO to get an (infinite) list of keypresses from stdin and then used this as the sequence of transformations. This is captured by the following code:

parseInput :: [Char] -> [Direction]
...
advance :: World -> Direction -> World
...
input <- getContents
let states = scanl advance initialWorld (parseInput input)

The last two lines are from the main function, and the preceding lines are the type signatures necessary to understand them. We can see that we first take the raw input from the user (via the getContents IO action) and parse the sequence of raw keypresses (the infinite list of Char) into a sequence of Directions in which to move the snake. We then do a left scan of the advance function over this sequence of directions, starting with the world in its initial state, to generate a sequence of states of the world! parseInput also handles quitting the game when the user presses q. We model this by terminating the sequence of directions when we detect that q was typed.

Once we have this sequence of game worlds we just need to draw them to the screen. Naively I initially did the following 1:

drawWorld :: World -> IO ()
...
mapM_ (\s -> clearScreen >> drawWorld s) states

i.e. I cleared the screen before drawing the new state. Unfortunately this caused the screen to flicker every time the world state updated, and I guessed (correctly) that it was because of the clearScreen taking just long enough to be noticeable. My solution was instead to "update" the screen:

drawUpdate :: (World, World) -> IO ()
...
mapM_ drawUpdate $ zip states (tail states)

drawUpdate is actually pretty dumb; it just "deletes" the snake in the previous world by writing a space character to every position the snake occupied, then draws the snake position in the new world by writing a @ at every position it occupies.

The result can be seen below

This is smashing, but is clearly not really a snake game yet! We have to add a few more ingredients to make it more like the game I remember from the old Nokia phones.

Adding extra ingredients

The first thing to do was to actually make it possible to lose the game. This involved detecting collisions between the snake and itself or with the boundary. After these additions I had something that looks like this:

The final piece of the puzzle (for now) was to add the food that could be eaten and would reappear in a random location. This led to the final iteration:

This is already starting to look a lot like what I had initially envisioned! The next step (which I will detail in a subsequent post) is to make the snake move in the last direction selected every second or so. This will probably require a rewrite of much of the code; we'll need to have another "source" for direction commands, and probably different threads to to do the waiting.

Thoughts

Writing this short program was really a lot of fun. In addition, it taught me a bunch of stuff about writing Haskell programs! Below are a few points that I came to appreciate during this project.

Type signatures are your documentation

I was startled by how much intention could be gleaned just from the type signatures and sensibly naming the functions. Given that I had some context about the program as a whole I found that the meaning of most of the functions became self evident. For example, given that I know that the World datatype contains the state of the game world, and Direction is an order to move the snake in a particular direction, the meaning of

advance :: World -> Direction -> World

is obviously "advance the state of the game world in response to an order to move in a particular direction".

I realise that my perspective on this is pretty skewed due to the short length of the program I was writing (you can hold the context of the whole program in your mind at once), but I get the impression that even with longer programs this concept that the type declarations are (for many functions) your documentation is quite prevalent. This is very different to Python, for example, where we are encouraged to document every function and detail its parameters.

If your program compiles, chances are it is correct

I had read this claim on several places on the web and was initially sceptical, but can now anecdotally confirm it to be true! I reckon this surprising property of Haskell is due to the fact that Haskell programs naturally need to be decomposed into teeny weeny functions that do literally only one thing. As far as I can tell this need to decompose your program into much smaller pieces than you otherwise would is a consequence of Haskell's purity. We can't hold any mutable state in "variables", and each function returns an output and does nothing else, so there's not really much "room" to do much else than just quickly compute a value and return it. It thus becomes abundantly clear when you are writing a function whether it is correct or not, as you often only have to verify that a few expressions are correct.

Let's take the advance function (before we added the food) as an example. We want it to move the snake in a particular direction, unless that direction is the opposite direction to the direction in which the snake is currently moving (in which case advance should not change the state of the world). In code:

advance :: World -> Direction -> World
advance w newDir
    | newDir == opposite (direction w) = w
    | otherwise = World { snake = slither (snake w) newDir
                        , direction = newDir
                        }

The above code is obviously correct; if the new direction is opposite to the current direction, then just return the current world state, otherwise return a state of the world where the snake has slithered in the new direction. Of course we still need to verify that opposite and slither are implemented correctly, but because they have similarly restricted scopes it becomes just as easy to verify their correctness.


  1. mapM_ maps a function that returns an IO action (more generally, any monadic value), and then sequences those actions.