Home

Awesome

snake-fury

Introduction

Welcome to snake-fury: the Haskell challenge for beginners. This challenge aims to provide a learning path for people willing to learn Haskell further than basic recursion exercises by implementing the snake game in Haskell. Snake-fury's pedagogical approach is based on two legs:

The first leg means that you'll be asked to implement some functions/algorithms. It is expected that the challenger will be unable to implement them without some research on Hackage's documentation, blogs, youtube videos, etc. There will be guidelines to help the challenger. Nevertheless, an important skill when learning Haskell is to be able to search, read and understand the documentation that is (often, but not always) more complex and less accessible than other programming languages.

The second leg is even more interesting. Haskell is notoriously known for its difficulty and the popularization of the holy triad: Functor - Applicative - Monad. There are plenty of tutorials showing examples and hundreds of thousands of lines trying to make them accessible and newcomer-friendly... But with all due respect, It seems they all fail to explain: "Why monads? Why not other less mathematical abstraction? Why not classic OOP patterns?". The approach given by snake-fury is to make the same application twice... it sounds crazy, but the idea goes like this: You'll implement a "pure" version of the snake game: No monads, no functors, no abstractions [see below]. Then you will refactor the core application logic using the state and reader monads. Then you'll be asked to abstract your code and to use mtl classes to make your code less dependent on the concrete implementation.

Below there is a dramatization of Haskell's learning curve. This challenge aims to be a helpful companion from the newby slope to the temple of oblivion... but be aware, nothing will save you from the temptation of abandon. Hopefully, you'll be able to climb up to the temple and spread the lambdas

dramatization of Haskell's learning curve

note about not using monads

By that I mean, not using do notation nor functor/applicative/monads combinators like liftA2, fmap, >>=, etc... Obviously, The IO and the asynchronous part of the code are provided and the challenger is not expected to solve it.

Start coding: Building a MVP

First you need access to a Haskell development environment. You can either:

Once you have a dev-env up and running, clone the code and move to snake-fury-exercise branch

git clone https://github.com/lsmor/snake-fury.git
git checkout snake-fury-exercise

# Optionally, you can create a solution branch out of this one
git checkout -b my-solution # (example name)

You should see this folder structure (among other files)

app
 |- Main.hs            # (implemented)     the entrypoint of your application.
src
 |- EventQueue.hs      # (implemented)     here is the EventQueue.
 |- GameState.hs       # (not implemented) here will go the logic of the game.
 |- RenderState.hs     # (not implemented) here will go the data structures for rendering the game
 |- Initialization.hs  # (implemented)     some utility functions

Exercises are comments in the files:

They can be filled in any order.

Each file correspond to each component in the system (and some utilities to keep code simpler). Be sure you read the architecture section to understand why the code is structured this way. Notice that you'll need to implement as many auxiliary functions as you need to make it work. If you feel stuck you can check the solution I've implemented.

Once you complete all exercises in GameState.hs and RenderState.hs you should be able to run the snake game in a terminal with the command below.

# To move the snake, either
#   use arrow keys: ← ↑ → ↓
#   use vim motions: hjkl
#   use gamer motions: wasd

# cabal users
cabal build
cabal run snake-fury -- height width fps # A common set up is a 10x10 board running at 6 fps

# stack users
stack build --stack-yaml stack-ghc-<your.system.version>.yaml
stack run snake-fury -- height width fps # A common set up is a 10x10 board running at 6 fps

example of running

Continue coding: Refactors

You've built a minimum viable product. Now it is time to improve it!. In the exercises folder you have intructions for refactoring. Follow them one by one as enumerated. One of the ideas is you feel the power of the type system when refactoring. Also you'll be dealing with poor design choices implemented in the mpv part.

Solution branches

You have multiple solutions branches, one for each refactor step and one for the mvp. Below you have the relation of branches and refactors. You can run git branch -l to see the list of all branches.

My solution might be different than yours, and that is totally fine. All solution branches should build and run the same as your exercise branch.

note 1: main branch is unstable an you shouldn't build it. Contains all sort of experiments, including an heavy sdl front-end

note 2: At the moment, the main branch doesn't use frames per second but microseconds as input parameter. Hence cabal run snake-fury -- 10 10 100000 will run at 10 fps. In the exercise branch, the input parameter is correct. So if you want to build from main be aware of this change.

Architecture

The general architecture of the software is the following:

Notice that two threads are necessary, since user could press keys faster than the game updates (fps). For example let say we run a frame each second and a half (normal speed in the snake game), then a user is likely to press keys faster than that. If the key strokes are catched only when a frame is about to be rendered, then many strokes will be lost.

The following diagram helps to visualize

Overview of the architecture

Contributions and Feedback

Please, open an issue if you:

If you want to contribute with a PR, take into account that due to the refactoring nature of the challenge, there are many branches. This makes integration difficult but you should follow these two rules:

The branch graph look like:

main
 |-solution-mvp
   |-snake-fury-exercise
   |-solution-refactor-1
     |-solution-refactor-2
       |-solution-refactor-2.2
         |-solution-refactor-3
           |-solution-refactor-4.1
             |-solution-refactor-4.2
               |-solution-refactor-5

Also I'd appreciate if you write some feedback for me. As already said, open an issue with a label feedback. These issue won't be closed, as it is easier for someone comming to the challenge to filter by open issues with label feedback, rather than closed issues. Please, do not use the feedback issue for any other than providing feedback, so if you have an idea on how to improve the challenge open a second issue with your proposal. Ideally feedback issue will have a maximum of two comments: the feedback itself, and a second comment in case the issue is outdated due to changes. Avoid meaningless feeback if possible.

Positive feedback should answer these questions:

Negative feedback (is welcome) should answer these questions:

Here are some examples:

Wrong positive feedback

This challenge is cool!


Right positive feedback

This challenge is cool!. I learnt some Haskell a year ago but could not build anything usefull with it (so I was in the valley of frustration and abandom the language). Monads are much clearer now, I am still wrapping my head around mtl style, but sure I will get it!!.


Wrong negative feedback

This challenge is worthless


Right negative feedback

This challenge is worthless. I've being learning Haskell for the last 6 months and still I am unable to build anything usefull. Building the mpv is doable (but very difficult), but when you start with the first refactor you end up touching the f**** IO monad and using ByteString/Builder. Why on earth Haskell hasn't a good default string library??.

This challenge should make an effort to explain all the inconviniences newcommers should face when getting into the language.

Thanks for reading