Please Buy now at Leanpub
Written and illustrated by GetContented.
Published on 2017-06-25.
We’re going to introduce you to a tiny toy educational game that introduces quite a few new functions and features of Haskell.
We won’t be using any special algebraic data types in this program. In particular, our main type will be:
-- a game Level is simply a pair -- of Integer values type Level = (Integer, Integer)
Level is simply a pair that has two
Integer values. As this is a simple math game, each level will be a pair of numbers that represent the two numbers for a multiplication question. (For example, a level value of
(7,9) would represent a question something like “What is 7 times 9?”).
The next definition is where we build the levels for this game:
levels :: [Level] levels = concat $ map pairsForNum [3,5..12] where pairsForNum num = zip [2..12] $ repeat num
Wow, there is a lot packed into this value expression. Let’s pull it apart piece by piece.
levels value is a list of
Level values. It’s defined using the
($) function, which takes a function on the left, and applies it to the value or expression on the right.
In our case here, the value expression on the right is
map pairsForNum [3,5..12]. The main new thing in this expression for us is
[3,5..12], which is what’s called a range. The range of numbers between 1 and 100, for example, would be represented as
[1..100]. The range above, though, is every number between 3 and 12, in odd numbers. (So, 3,5,7,9,11).
So what we’re doing, then, is mapping the
pairsForNum function across this range. This function takes a number, calling it
num, and “zips” the range
[2..12] together with
repeat num. Zipping is creating pairs from one item each of a number of lists as we’ll see. The
repeat function gives an infinite list of whatever its argument is. The
zip function takes two lists, and builds pairs out of those lists, taking one item from each as it does. So,
zip [1,2,3] [4,5,6] will create
[(1,4),(2,5),(3,6)]. However, the
zip function will stop when the first list runs out of values, so zipping an infinite list of repeated items with another that has only a few is just fine, as we’re doing.
So what we’ll end up with by using
map pairsForNum [3,5,..12] is a list of lists of pairs of
Integer values. This basically means we’re combining each of the elements with each of the others. Then, we use
concat to concatenate all the lists into one list. For example,
concat [,,] results in
[1,2,3]. In math, this process of creating a big set out of two smaller sets is called a cartesian product. You don’t have to know this, but later we’ll see other, much simpler-looking, easier ways to do this. Unfortunately using them requires knowing more than we currently know, so that will have to wait.
The end result of this expression is that we have a list of 55 levels for our train of terror!
Here’s a function we’ll use in our program to work out what number level the player is at:
levelNumber :: [a] -> Int levelNumber remainingLevels = totalLevels - levelsLeft where totalLevels = length levels + 1 levelsLeft = length remainingLevels
We take a list of remaining levels, then subtract the number of levels left (using the length function) from the number of levels we started with plus one. As the player “moves up” the train, we’ll be reducing the size of the list, so the remaining levels will get less, and the level number will go up. Notice from the type signature that we don’t care what the element type of the list passed into it is, as long as it’s a list.
main function simply gives the player a greeting message, then starts the
trainLoop function that handles all the game-play (by passing in the levels value).
main :: IO () main = do putStrLn "Suddenly, you wake up. Oh no, you're on..." putStrLn "The Times-Table Train of Terror!" putStrLn "Try to get to the end. We DARE you!" trainLoop levels
You can see we have our old friend the
do block in action again, joining a whole bunch of IO actions together to form one.
In a moment, we come to the main game-play function,
trainLoop has two definitions.
The first is for an empty list, which for our program means the player has won, because they got to the end of the train without failing. In this case, we declare their victory to them.
The second definition comes into play when there are still levels passed in. This means the game is still in play. Each time the player gets done with a level, we remove it from the list, then pass the rest of the levels back into the trainLoop function and start it again.
trainLoop :: [Level] -> IO () trainLoop  = putStrLn "You won! Well done." trainLoop remainingLevels @ (currentLevel : levelsAfterThisOne) = do let currentLevelNumber = levelNumber remainingLevels (num1, num2) = currentLevel putStrLn $ "You are in a Train Carriage " ++ show currentLevelNumber ++ " of " ++ (show $ length levels) putStrLn "Do you want to:" putStrLn "1. Go to the next Carriage" putStrLn "2. Jump out of the train" putStrLn "3. Eat some food" putStrLn "q. Quit" activity <- getLine case activity of "1" -> do putStrLn $ "You try to go to the next carriage." ++ " The door is locked." putStrLn "Answer this question to unlock the door:" putStrLn $ "What is " ++ show num1 ++ " times " ++ show num2 ++ "?" answer <- getLine if answer == (show $ num1 * num2) then do putStrLn "The lock is opened!" trainLoop levelsAfterThisOne else do putStrLn $ "Wrong. You try to open the lock," ++ " but it won't open." trainLoop remainingLevels "2" -> jumpingFutility "3" -> eatingFutility "q" -> putStrLn $ "You decide to quit." ++ " Thanks for playing. Bah-Bye!" _ -> do putStrLn "That makes NO sense! Try again." trainLoop remainingLevels
The construction with an
@ symbol means the whole argument is matched as
remainingLevels, and then, also,
currentLevel is set to the first item of the list, and
levelsAfterThisOne is set to the tail of the list. When we use the
@ symbol like this, it’s called an as pattern.
trainLoop passes back an
IO action, so we begin a very large
do block. This
do block has lots of different kinds of things in it, to get you familiar with more complicated looking
Ideally, we would pull each of the sections into their own function. For now we’ll look at it like this because it serves our purposes quite nicely to show you a larger
Remember, each of these expressions will evaluate to an
IO action (because the final value of this function’s type is
IO ()), and the
do block simply connects them all up properly so they become one single
First, we set up some variables we’ll need later using
let expression is another way we can define some locally scoped definitions. In a
do block, name defined in a
let expression will be available for the remainder of the
do block. One of these is
currentLevelNumber which uses the
levelNumber function we just looked at, and the next line is pattern matching the
currentLevel variable into two number variables
currentLevel’s type is
Level, which is
(Integer, Integer), so
num2 will both be
Then we print out a message explaining that the player is on a certain train carriage, using show to turn the number values into strings so they can be fed into
(++) with the other strings.
Next, a menu is described, and we receive a line of text from the player with
getLine, which goes into the variable called
After this, we have a
case expression that matches on the player’s response. If they wrote 2, 3 or q, then we write a message and restart the whole game, or quit out (q quits the game).
If, however, they typed 1 in, they’re asked this level’s multiplication question. If they get it right, it unlocks the current level and they can proceed, which we do by starting the
trainLoop again but with only the tail of the levels (by using the
All that remains is to show the definitions for
eatingFutility. These actions are executed if the player tries to jump or eat.
All they do is print a message, then start the game at the very beginning by returning trainLoop applied to the initial levels.
jumpingFutility :: IO () jumpingFutility = do putStrLn "You try to jump out of the train." putStrLn "You fail and die." trainLoop levels eatingFutility :: IO () eatingFutility = do putStrLn "You see a delicious looking cupcake." putStrLn "You eat it. It's a time travel cupcake!" trainLoop levels
Write a program that prints your birthday on the screen. Write another program that prints two numbers added on the screen, then one for multiplied, then subtracted.
If you’ve enjoyed reading this, please consider purchasing a copy at Leanpub today
Please follow us, and check out our videos Follow @HappyLearnTutes
Also, Volume 2 is now in beta and being written! Show your support and register your interest at its Leanpub site.