Years ago when I was learning Haskell I had asked a user in #haskell-beginners what would be some good projects or perhaps a good path to getting more involved in open source projects. I must preface this was before the age of AI taking over. A user responded to update his game called TwosGame (original repository here) he made to showcase transformers, lens and machines packages.
So why now posting the update? When I first began my journey learning Haskell I I struggled with basic enhancements like updating a project to usearrow keys let alone more complicated libraries such as machines and lenses. The machines package allows us to compose a nice monadic stream processing pipeline. Now, having gained considerable more experience I am revisiting this project to do a small write up on the changes I had made. You can open up this commit and see what changes were made while we go through them.
What is TwosGame?
This is a terminal based implementation in Haskell of a similar game called 2048 by a user named glguy to demonstrate the machines and lens libraries.
The area of code we want to look at is line 263
vimBindings :: Process Char Command
vimBindings = repeatedly process1
where
process1 = do c <- await
case c of
'j' -> yield (Move D)
'k' -> yield (Move U)
'h' -> yield (Move L)
'l' -> yield (Move R)
'q' -> stop
'`' -> yield Undo
_ -> return () -- ignore
HaskellWe have a function called vimBindings that is a Processor that takes a Char as input and produces a Command as output. This works by using repeatedly which continuously runs the process, in this case process1 and handles running and termination of the process. This is a monadic function that handles the processing of a single character at a time, in this case one of the vim bindings for movement j,k,h,l which is what we need to update.
Since we are working with monadic operations we use a do block and structure our code to wait for a character. Once we have a character we will output (yield) a character to the next process in the pipeline if it matches with one of our case statements.
Updating the movement bindings
Now that we have an idea of what is going on let’s update this section of code to use arrow keys instead of vim bindings. What we first need to understand is what we need to change our case statement to so we need to look at how the special arrow keys are received. We can use evtest to see this.
travism@meerkat ~ → evtest
Available devices:
Select the device event number [0-0]:
^[[A
ShellSessionWe can see that we have first the escape character ^[ and then we have the ANSI escape sequence of [A, [B, [C, [D depending which arrow key was press. We can detect if the escape character is received and then a bracket and then finally check which character was received. This is what we need to add to our case statement to return a command based on A, B, C, D. Seems simple enough?
arrowBindings :: Process Char Command
arrowBindings = repeatedly $ do
c <- await
case c of
'\ESC' -> do
_ <- await -- disregard [
arrow <- await -- Check which arrow key was pressed
case arrow of
'A' -> yield (Move U)
'B' -> yield (Move D)
'C' -> yield (Move R)
'D' -> yield (Move L)
_ -> return ()
'q' -> stop
'u' -> yield Undo
_ -> return ()
HaskellPretty simple changes to process the arrow keys. We still utilize some of our previous code by first getting c <- await
which will match on the ‘\ESC’ , but we then check for the first bracket and we do not care about this value so we will use the _ to disregard the value, but we ensure that it is going to be a special key which all start with the escape sequence and a bracket. We now only care to check the alphabetical character which will signify which special key was pressed and from our test above we can see it is one of the four A,B,C,D characters. We will await
a value and assign it to arrow and then we do the same process and yield the value to the next process in the pipeline.
The last thing you can see that we did is we just updated the usage to show the user the up, down, left and right are now arrow keys.
usageText = termText "(←) left (→) right" <> nl
<> termText "(↓) down (↑) up" <> nl
<> termText "(`) undo (q) quit" <> nl
HaskellNow if we run build it with cabal and run it we can use arrow keys to move the blocks.
Have a look through the rest of the code, it uses some nice examples to show using lens’s, handling state and processing actions based on input.
My updated repository with the above changes here