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
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.
First you need access to a Haskell development environment. You can either:
stack
user, modify the hie.yaml
so the cradle is stack
not cabal
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:
src/GameState.hs
src/RenderState.hs
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
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.
mtl
like constraints. Now the code looks a little bit abstract but it is easy to read and followYou 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.
solution-mvp
branch corresponds to the solution of the first exercisesolution-refactor-1
branch corresponds to the solution of the first refactor in exercises/refactor-1solution-refactor-2.1
branch corresponds to the solution of the second refactor, step 1 in exercises/refactor-2solution-refactor-2.2
branch corresponds to the solution of the second refactor, step 2 in exercises/refactor-2solution-refactor-3
branch corresponds to the solution of the third refactor in exercises/refactor-3solution-refactor-4.1
branch corresponds to the solution of the fourth refactor, step 1 in exercises/refactor-4solution-refactor-4.2
branch corresponds to the solution of the fourth refactor, step 2 in exercises/refactor-4solution-refactor-5
branch corresponds to the solution of the fifth refactor in exercises/refactor-5My 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-endnote 2: At the moment, the
main
branch doesn't use frames per second but microseconds as input parameter. Hencecabal 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 frommain
be aware of this change.
The general architecture of the software is the following:
(user keyboard) -> EventQueue -> GameState -> RenderState -> (render device)
EventQueue
EventQueue
, and based on what the user has pressed, it runs the game logic and prints the board in the consoleNotice 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
Please, open an issue if you:
cabal
. Add label solution branch does not build to the issueIf 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:
main
branch. This rule apply to README, any markdown under exercises
, and any file in the assets
foldersolution-mvp
if the change applies to all refactors; Or merge into solution-refactor-XXX
if the change applies only to that refactor, hence you should branch out from the corresponding branch. Any change done in solution-mvp
is merge into snake-fury-exercise
and solution-refactor-XXX
.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