Making a Snake game in F#
This blog post has been sitting in my drafts for over a year. I finally found someone to take a look at it and annoy me to post it. Thank you to the editor!
A year ago I decided I wanted to build a Snake game. To get started I headed over to Rosetta Code to find an example algorithm just to whet my appetite. To my great horror there was no F# example. When I googled around online, I found great examples, but they used Fable (which is an awesome project) or were too big to fit into a small code example. I wanted something that only used System libraries packaged with F# & dotnet — nothing fancy — and it needed to be under a few hundred lines of code to fit with the rest of the samples on Rosetta Code. Most importantly it had to be readable and clear so people could understand algorithmically what is going on. I decided I’d use the Console class and make the entire thing on the command line.
I consider myself a polyglot, but in practice my preference is F#. These days, F# just has more value for the developer than a lot of other runtimes & ecosystems out there. Professionally, I’m all over the place. I have been paid to write F#, but I wouldn’t say I created any Great Works™ during my professional time in dotnet. TypeScript, Python, JavaScript, Java, Ruby, Go — these are just a few of my favorite daily footguns. I am not sure how much human innovation is required for me to feel smart enough to write JavaScript. It might be simpler for humanity to become an interstellar species or cure cancer.
I figured I might as well just write it without copying a reference. I’m self taught and probably 99% of the time have no idea what’s going on. If I was wrong, well maybe if it “looked” right that would be enough. Unfortunately, I am a Luddite who still has not configured Github Copilot for their editor so we’ll be figuring this out ourselves and hoping for the best. Nobody has ever asked me to make a snake game in a job interview, though I suppose they could have. I’ll let you debate in the comments if that would even be a good idea. In my opinion, it’s not a bad idea at all, you’d just want to think about scaling the question across levels while also providing the ability of increasing the difficulty for senior individual contributors.
A snake, in my mind, sounded like a fancy list data structure — could be a ‘mutable list’ — a.k.a. array or dictionary — to make a stack, but didn’t need to be. Mutability would provide nice space constraints, but the good part of writing functional, immutable code in F# is garbage collection and support for tail-call optimization. I decided to make judgement calls on mutability, paradigm, and approach depending on the section. I have no idea why functional programmers get so heated about side effects. In my opinion, it’s about guard rails and “debuggability”. There is no perfect tool, there are only tradeoffs.
Anatomy of a Snake
Simply put: the snake needs food and a grid to move on. And what is a move exactly? A move is a direction. If a user presses an arrow key, the snake should move in that direction, otherwise the snake should automatically move itself forward in the direction it was already going. Okay, easy money, I said to myself. Time to go ahead and do some domain modeling…
I could do some fancy graphic designing here, but I wanted to just use emoji and keep it simple. I made the snake have a head, belly, and tail because I wanted the snake to have a sweet, green face 🐍 If I hadn’t wanted something fun, I probably would have just made the type without a belly. I thought a nice touch would be to randomly pick a type of food to drop on our grid. After all, snakes eat mice, rats, birds, frogs, bugs, and eggs!!
Our grid is just a two dimensional array. Moving up, down, left, and right were as simple as constant time manipulation of indices over the linear body length of the snake. We now can generate food and drop it on the grid on an unoccupied space. Nothing here would be out of the ordinary in most languages on the market. The only thing worth pointing out here is how excellent the type inference the editor is giving me. Note, for the reader’s clarity I have annotated the return types manually, but they are unnecessary. If I did not use them, Ionide editor would supply them as comments just like you see highlighted comments doing in other places. If the types are not accurate, the program will not compile. This is a massive advantage for refactoring safely and generally just getting things right the first time and every time.
The other point of interest for the viewer here is pattern matching expressiveness. In JavaScript most of us discovered how wonderful the idea of a switch statement is, but then plummet into the valley of disillusionment when we grapple with the implementation of the switch statement. There are quirks about JavaScript switch alongside the understanding that it isn’t quite powerful enough to do constraint based control flows, arbitrarily nested structure comparisons, guarded statements, exhaustiveness checking (ensuring you have accounted for all permutations of the thing you are trying to match on), and recursive structure support.
In F# I’m able to declaratively speak about what will happen in my program logically. I use this same approach with the inner recursive loop that allows the snake to move on the grid.
The entire game cycle elegantly wraps up into a recursive loop which matches on two ideas: GameRunning or GameOver. You can see our guarded pattern matches here looking for a few constraints: Graphics.isOccupied (the snake hit its own belly or a wall), Graphics.isFood (the snake hit something it can eat), or neither of those conditions and so we now pivot the snake in the valid direction it was moving before.
I’ve never built a game before, but this seemed like the basic idea of what a game was. For a lot of people, recursion isn’t a comfortable idea, but after a while of working in these types of languages, it’s my preferred way of programming. If you think you can help me cut down the line count without sacrificing readability, please contribute back :]
Poorly Formatted Donation to Rosetta Code: https://rosettacode.org/wiki/Snake#F#