1 Introduction

Later in the course you will learn how to write graphical user interfaces, and if you want to you can then write such an interface for this program. To cater for this possibility, try to write your program in such a way that you can understand it in a month's time.
2 The game
The game you will implement is a simple variant of Black Jack. There are two players, the “guest” and the bank. First the guest plays. She (he) can draw as many cards as she wants, as long as the total value does not exceed 21. When the guest has decided to stop, or gone bust (score over 21), the bank plays. The bank draws cards until its score is 16 or higher, and then it stops.The value of a hand (the score) is the sum of the values of the cards. The values are as follows:
Numeric cards have their numeric value, so a nine is worth nine points.
Jacks, queens and kings are worth ten points each.
Aces are worth either one point or eleven points. When calculating the value for a hand, all aces have the same value; either they are all worth eleven points, or they are all worth one point. Initially the value eleven is used for the aces, but if that leads to a score above 21, then the value one is used instead.
3 Assignments
In summary, your task is the following:Compute
size hand2
(see Section 3.1) by hand, step by step.Implement the Haskell functions and QuickCheck properties listed below.
While writing the functions (not afterwards), document them.
3.1 Recursive types
We provide two Haskell files that you should use, Cards.hs and RunGame.hs. You do not need to understand anything about RunGame, but you have to understand most of Cards. That module contains definitions of data types used to model cards and collections of cards (hands). Read through the file so that you know which types you are supposed to use. (You do not yet need to understand theArbitrary
instances in Cards.)
One of the types is more difficult than the others:
Adata Hand = Empty | Add Card Hand deriving (Eq,Show)
Hand
can be either
Empty
, i.e. an empty hand, orAdd
card hand. Here card is aCard
and hand is anotherHand
, so this stands for a hand with another card added.
We start with the empty hand, add the jack of spades, and finally add the two of hearts. It is tiresome to write down all 52 cards in a full deck by hand, though. We can do that more easily by using recursion. That is left as an exercise for you, see below.hand2 = Add (Card (Numeric 2) Hearts) (Add (Card Jack Spades) Empty)
We can use recursion both to build something of a recursive type (like a deck of cards) and to take it apart. For instance, say that you want to know the size of a Hand
. That is easily accomplished using recursion and pattern matching. Note the similarity with recursion over integers:
At this stage in the course you might not have fully grasped recursive types; that is after all the reason for doing this assignment. To get a better idea of what happens when a recursive function is evaluated, take some time to work withsize :: Num a => Hand -> a size Empty = 0 size (Add card hand) = 1 + size hand
size
before you continue.
size hand2
by hand, on paper. The result should be 2
, right?
Write down this sequence of equations in the beginning of a Haskell file called BlackJack.hs, as a comment.size hand2 = size (Add (Card (Numeric 2) Hearts) (Add (Card Jack Spades) Empty)) = ... = 2
The function size
is included in the Cards module.
You can find more recursive functions in the lecture notes.
3.2 Properties
To help you we have included a couple of QuickCheck properties below. Your functions must satisfy these properties. If they do not, then you know that something is wrong. Testing helps you find bugs that you might otherwise have missed. Note that you also have to write some properties yourself, as indicated below.The purpose of writing properties is three-fold:
- They serve as a specification before you write your functions.
- During the implementation phase they help you with debugging.
- When you are finished they serve as mathematically precise documentation for your program.
3.3 Documentation
The code has to be documented. See the RunGame and Cards modules to get an idea about what kind of documentation is expected of you. Try to follow these guidelines:Focus on what the function does and how one can use it, but not on how the function is implemented. Of course, if a function is complicated then the implementation also has to be documented, but you are not supposed to write complicated functions in this assignment.
Try to keep the documentation as short as possible, without sacrificing clarity. Long comments make the code harder to read. Similar arguments apply to documentation as for properties; write the documentation when you write your functions, not afterwards.
3.4 Functions
Write all code in a fresh file called BlackJack.hs. To make everything work, add the following lines in the top of the file:This tells the Haskell system that the module is called BlackJack, and that you want to use the functions and data types defined in Cards and RunGame. Download Cards.hs and RunGame.hs and store them in the same directory as BlackJack.hs, but do not modify the files.module BlackJack where import Cards import RunGame
You have to implement the following functions:
empty :: Hand
value :: Hand -> Integer
- (Option 1) Define a function
initialValue :: Hand -> Integer
that uses 11 for the value of aces, and a functionnumberOfAces :: Hand -> Integer
that can be used when computing the final value, if the initial value is over 21. - (Option 2) Define a function
valueWithValueOfAce :: Integer -> Hand -> Integer
which takes the value you want to use for the aces (1 or 11) as an extra argument. Call this function to compute the initial value, and then call it again to compute the final value, if the initial value is over 21.
valueRank :: Rank -> Integer valueCard :: Card -> Integer
gameOver :: Hand -> Bool
Herewinner :: Hand -> Hand -> Player
Player
is a new data type, defined in the
RunGame module:
data Player = Guest | Bank deriving (Show,Eq)
<+
puts the first one on top of the second one: (Note that a function name with only symbols indicates an infix operator. It is used just like(<+) :: Hand -> Hand -> Hand
+
or −
, with the operator between its arguments: h1 <+
h2.)
This function must satisfy the following QuickCheck properties. The function should be associative:
Furthermore the size of the combined hand should be the sum of the sizes of the two individual hands:prop_onTopOf_assoc :: Hand -> Hand -> Hand -> Bool prop_onTopOf_assoc p1 p2 p3 = p1<+(p2<+p3) == (p1<+p2)<+p3
The implementation of this property is not given here, you have to write it yourselves.prop_size_onTopOf :: Hand -> Hand -> Bool
You could do this by listing all 52 cards, like we did with two cards above. However, that is very tedious. Instead, do it like this: Write a function which given a suit returns a hand consisting of all the cards in that suit. Then combine the 13-card hands for the four different suits into one hand usingfullDeck :: Hand
<+
.
If the deck is empty, report an error usingdraw :: Hand -> Hand -> (Hand,Hand)
error
:
By changing the type oferror "draw: The deck is empty."
draw
one could get around this rather ugly solution. We will get to that later in the course. Maybe you can think of a way already now?
To return two values a and b in a pair, use the syntax (a,b). You can also pattern match on pairs, like in this example:
first :: (a, b) -> a first (x,y) = x
To write this function you will probably need to introduce a help function that takes two hands as input, the deck and the bank’s hand. To draw a card from the deck you can use where in the following way:playBank :: Hand -> Hand
playBank' deck bankHand ... ... where (deck′,bankHand′) = draw deck bankHand
StdGen
and a hand of cards, shuffle the cards and return the shuffled hand:
shuffle :: StdGen -> Hand -> Hand
Note: if you importTest.QuickCheck
in this module, to avoid a name clash, do it like this:import Test.QuickCheck hiding (shuffle)
A StdGen
is a random number generator.
Import the System.Random
library:
Now, if g is a random number generator, thenimport System.Random
randomR
(lo,
hi) g is a pair (x,g′), where x is a number between lo and hi (inclusive), and g′ is a new random number generator. Note that to get several random numbers you have to use different random number generators; if you used g this time, then you have to use g′ (or some other generator) the next time. If you were to reuse g then you would get the same result again.
As an example, the following function takes a random number generator as input and uses it to calculate two random integers between 0 and 10, inclusive:
Note that if we had usedtwoRandomIntegers :: StdGen -> (Integer,Integer) twoRandomIntegers g = (n1, n2) where (n1, g1) = randomR (0, 10) g (n2, g2) = randomR (0, 10) g1
g
in the last line as well, then n1
and n2
would be equal, so instead we use the new random number generator g1
returned by randomR
.
By the way, you can construct a value of type StdGen
by using mkStdGen :: Int -> StdGen
.
So, now that we know how to handle random numbers, how can we shuffle a deck of cards? If you want a (small) challenge, do not read the next three paragraphs.
One way to shuffle the cards would be to pick an arbitrary card from the deck and put it on top of the deck, and then repeat that many times. However, how many times should one repeat? If one repeats 52 times, then the probability that the last card is never picked is about 36%. This means that the last card is often the same, which of course is not good.
A better idea is to pick an arbitrary card and put it in a new deck, then pick another card and put it on top of the new deck, and so on. Then we know that we have a perfectly shuffled deck in 52 steps (given that the random number generator is perfect, which it is not).
Note that for both approaches we need a function that removes the n-th card from a deck.
The function shuffle has to satisfy some properties. First, if a card is in a deck before it has been shuffled, then it should be in the deck afterwards as well, and vice versa:
For this we need the helper functionprop_shuffle_sameCards :: StdGen -> Card -> Hand -> Bool prop_shuffle_sameCards g c h = c `belongsTo` h == c `belongsTo` shuffle g h
belongsTo
, which returns True iff the
card is in the hand.
(By usingbelongsTo :: Card -> Hand -> Bool c `belongsTo` Empty = False c `belongsTo` (Add c' h) = c == c' || c `belongsTo` h
`
we can turn a function into an infix operator.)
The above property does not guarantee that the size of the deck is preserved by shuffle; all cards could be duplicated, for instance. You have to write a property which states that the size is preserved:
prop_size_shuffle :: StdGen -> Hand -> Bool
3.5 Interface
runGame
function with the package as an argument.
To “package up” these functions, write the following code:
To run the program, defineimplementation = Interface { iEmpty = empty , iFullDeck = fullDeck , iValue = value , iGameOver = gameOver , iWinner = winner , iDraw = draw , iPlayBank = playBank , iShuffle = shuffle }
in your source file, load the file, and runmain :: IO () main = runGame implementation
main
.
4 Possible extensions
The following is completely optional, but if you want to do more, there are many possibilities:Now the bank draws all its cards after the guest has finished. It would be more fun if the guest could see the bank’s cards while playing.
The rules are not really proper Black Jack rules.
There are many other card games, many of which may be more fun than this one.
You probably have some ideas yourself.
Note that doing something extra will not directly affect your grade, but can of course be beneficial in the long run. Take care to do the compulsory part above before attempting something more advanced, though.