In this refactor you'll modify the code to add a score board and to accelerate snake's speed as you get points. Also we'll point out a performance problem with the renderer. Your mission is to solve it.
This refactor has three steps:
you will update the code to make a score which is updated each time the snake eats an apple. The score is rendered in the terminal above the board. (if you prefer another rendering is up to you. Shouldn't be difficult to custom you own builder).
- Add a
score :: Int
field toRenderState
. You can add it toGameState
too, but since we usescore
solely to render it on the screen and to speed up the fps, it makes sense to keep it only in the rendering side of things. - Once you add
score
, try to compile the code. Follow the errors until you got the game working again. Now let's implement the logic.
- Because
score
is used inRenderState
we need to update it viaRenderMessage
. Add a new case in theRenderMessage
ADT to represent such message. Does the compiler complain about incomplete patterns? Which function/s is broken after this change? - If you think carefully, we need to change
GameState.move
function, because now there are cases in which we need to send more than one message. Update the code to send a list of messages after each move. - Now, something is broken... we used to proccess a single message in
RenderState.updateRenderState
. Create a new functionupdateMessages :: RenderState -> [RenderMessage] -> RenderState
which updates the state, given many messages. Hint: it can be a one-line function. - clean compiler errors if any.
In this part you'll be asked to modify some code of the EventQueue
, which runs in the IO
monad. So we can say that this will be your first contact with monads. Congrats! The only problem is that EventQueue
is an asynchronous queue of events, so it can be a little bit too much for your first contact. Read carefully and don't worry if you need to sneek out the solution doing git checkout solution-refactor-1
.
- Take a look at functions
EventQueue.calculateSpeed
andEventQueue.setSpeed
. The former is a pure function which calculates the speed given the score and the initial speed. The second is a function in theIO
monad. Try to understand it as better as you can, but don't worry if you can't fully understand it. - Take a look at function
Main.gameloop
. The first line looks likethreadDelay $ initialSpeed queue
. Let's go step by step:initialSpeed queue
accesses theinitialSpeed
field within theEventQueue
. Unsurprisingly, this is the speed you set up when running the code.threadDelay
essentialy stops the execution for a given number of microseconds. Therefore, this function looks at the speed you set when initializing the game and waits that much time. - Your mission is to modify that part of the code to get the speed based on the
score
and wait that much time. Two changes are needed- the first line now should modify the speed. Use the function
setSpeed
. Given the right arguments it returns the new speed in a monadic context! To access that value you have to bind it:new_speed <- setSpeed <args>
- Modify the second line (
threadDelay $ initialSpeed queue
) so now you wait the right amount of seconds.
- the first line now should modify the speed. Use the function
- clean errors the compiler gives if any.
Now if you compile the code, you should notice that every 10 points the speed increases by a 10%. You can tweak this configuration in the EventQueue.calculateSpeed
function.
If you run a big enough board or at high speed, you'll notice that the game starts to blink, or the board is rendered slowly. This is because the function render
returns a String
. This type is an historical mistake within Haskell ecosystem and should be banned. Unfortunately, this would break lot of code, so we have to live with that. String
is a linked list of Char
which is a very inefficient way of representing textual data. You should never use String
... never! There are other many representations of textual data such as Text
(utf-8), ByteString
(raw bytes decoded as ascii), Builder
(a buffer in memory), and lazy variations of each. Yes, the Haskell ecosystem for textual data is a mess.
For this challenge we are going to use a not very common representation of text: ByteString.Builder
. The reason for that is because we are transforming the RenderState
by concatenating strings text. Builder
is a very efficient type for concatenation.
- Read the documentation of
Data.ByteString.Builder
frombytestring
package. - Create a function
ppScore :: Int -> Builder
which pretty prints the score. Feel free to use a representation you like. Below you have some examples - Modify the function
render
to have type:: BoardInfo -> RenderState -> Builder
. Of course, the render function should plot the score and the board itself - clean errors the compiler gives. How do you print a
Builder
into the console? (hint: checkIO
functions in theData.ByteString.Builder
module)
Here are some ways you can render the score:
# With stars lines # With a board # With motivation quote changing every 10 points
******** |--------| score: 5 / do better!
score:10 |score:10| score: 10 / keep going!
******** |--------| score: 20 / on fire!