Lab: Blackjack - part B
- Due 18 Sep 2020 by 17:00
- Points 0
- Submitting a file upload
- File types hs
- Available until 29 Sep 2020 at 17:00
This assignment is the continuation of Lab 2: Blackjack - part A and describes the second part (B). You will continue with your Blackjack.hs
file. There are also some extra optional extensions mentioned at the end of the assignment. These are not obligatory, but you will learn more when you do them!
Properties
To help you we have included a couple of QuickCheck properties in this part of the lab. 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.
So, to take maximum advantage of the properties, write them before you write the corresponding functions, or at the same time.
When using QuickCheck you need to first import it. However, it turns out that recent versions of QuickCheck define a function shuffle
whose name clashes with the function you are supposed to define in task later on (in B4). To avoid this problem, you can import QuickCheck as follows:
import Test.QuickCheck hiding (shuffle)
If you get an error about shuffle here it probably means you have an older version of QuickCheck, in which case just comment out the hiding (shuffle)
part.
Task B1
You also need to define a function that returns a full deck of cards:
fullDeck :: Deck
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 that returns a list of all possible ranks, and a function that returns given all possible suits, and then combine their results.
Your fullDeck
function must satisfy the following property:
prop_size_fullDeck :: Bool prop_size_fullDeck = size fullDeck == 52
Task B2
Given a deck and a hand, draw one card from the deck and put on the hand. Return both the deck and the hand (in that order):
draw :: Deck -> Hand -> (Deck, Hand)
If the deck is empty, report an error using error
:
error "draw: The deck is empty."
Hint: To return two values a
and b
as a pair, use the syntax (a, b)
. This syntax is also used for pattern matching on pairs:
first :: (a, b) -> a first (x, y) = x
Task B3
Given a deck, play for the bank according to the rules mentioned in the previous assignment (starting with an empty hand), and return the bank’s final hand:
playBank :: Deck -> Hand
To write this function you will probably need to introduce a helper function that takes the deck and the bank’s hand as input. To draw a card from the deck you can use where
in the following way:
playBank' deck bankHand ... ... where (deck', bankHand') = draw deck bankHand
You can read more about where
in the FAQ.
Task B4
This is the hardest part of the lab. Write a function that shuffles a hand deck of cards. The function shuffle
should take a deck of cards and return a deck with all the cards placed in random order.
Since Haskell functions are functions in the mathematical sense, they always give the same result if you give them the same argument. This means it is impossible to write a Haskell function that takes a deck of cards and returns a randomly shuffled deck!
The way we will sidestep this problem is to define a function which takes two arguments, the deck to be shuffled (as second argument) and a parameter representing the “randomness” as an input.
To understand how this will work, imagine that you want to take a random walk in a maze. Do this you are given really big stack of coins which have been randomly flipped and piled in a stack. Whenver you have to make a choice between going left or right you look at the coin at the top of the stack: heads (krona) and you go left, tails (klave) and you go right. But to make your walk random you must discard the top coin after you have used it. Assume that the stack is big enough that you never run out. Your path is a function of the coin stack that you had as input.
You will implement a shuffle function is a similar manner. Instead of a stack of coins you will have a list of floating point numbers (type Double
) in the range 0 to 1. You may assume that this list is big enough that you will never run out of random numbers to use, no matter how long you shuffle. For your convenience, Cards.hs will generate lists of floating point numbers from 0 to 1 to give it to the shuffle function as the first argument. (To make quickCheck able to do its work, we limit the list to 52 numbers, so don’t over-shuffle!).
Define the function:
shuffle :: [Double] -> Deck -> Deck
Optional Task 4.1
If you want a (small) challenge, try to implement shuffle
without reading the following hints. Hint (Shuffling strategy): One way to shuffle the cards would be to pick an random 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 randomly pick a card from the deck 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 input list of numbers is perfectly random, which it is not). Remember of course that to "pick a random card" we will use the list of random values that shuffle gets as its first argument.
Note that for both approaches we need a function that removes the n-th card from a deck.
Task B5
The function shuffle
has to satisfy some properties. In this task we give an example of one useful property, and you are required to define a second property of the shuffle function.
-
If a card is in a deck before it has been shuffled, then it should be in the deck afterwards as well. To write that as a property we need the helper function
belongsTo
, which returnsTrue
if and only if the card is in the hand.belongsTo :: Card -> Deck -> Bool c `belongsTo` [] = False c `belongsTo` (c':cs) = c == c' || c `belongsTo` cs
(Here we’re using
`
to turn a function into an infix operator just to make the code more natural to read.)With this helper function we can now define the property that shuffling does not change the cards in a hand as:
card `belongsTo` deck == card `belongsTo` shuffle randomlist deck
randomlist
that we need for testing should be an infinite list of values between zero and one, and not just any old list of doubles. The way we get around this is that we have defined a new type inCards.hs
:data Rand = Rand [Double]
More importantly, for this new type we have told quickCheck how to generate infinite lists of the right kind! (Note: the actual type of
We define the shuffle property as follows:Rand
is more complicated than this, but you should think of this definition when you use it).prop_shuffle :: Card -> Deck -> Rand -> Bool prop_shuffle card deck (Rand randomlist) = card `belongsTo` deck == card `belongsTo` shuffle randomlist deck
-
But this property still allows for errors. For example the above property would hold even if the shuffle function added duplicate cards to the deck.
The second property, the one you must define in this task, is a property to check that the size of the deck does not change when you
shuffle
.
This would allow you to check that you don’t accidentally duplicate cards. Complete the following definition:prop_size_shuffle :: Rand -> Deck -> Bool prop_size_shuffle (Rand randomlist) deck = undefined
Interface
You have barely touched upon input/output in the lectures, so we have to provide you with a wrapper that takes care of those things. All you have to do is to write the functions above, package them together (as explained below), and then call the wrapper with the package as an argument.
Task B6
To “package up” these functions, write the following code:
implementation = Interface { iFullDeck = fullDeck , iValue = value , iDisplay = display , iGameOver = gameOver , iWinner = winner , iDraw = draw , iPlayBank = playBank , iShuffle = shuffle }
(Note that Interface
is just an ordinary data type, here constructed using record syntax.)
To run the program, define
main :: IO () main = runGame implementation
in your source file, load the file in GHCi, and run main
.
Best of luck!
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.
Most of the ideas above require that you program input/output (I/O) yourself. You have seen how to do simple I/O in the lectures. Use the RunGame.hs module as a starting point.
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.
Submission
Write your answers/solutions in one file, called Blackjack.hs
. For each task, use Haskell comments to indicate what part of the file contains the answer to the tasks. For answers in natural language, you can use Swedish or English; write these answers also as Haskell comments. Please do not submit the modules Cards.hs
and RunGame.hs
(unless an extra assignment required you to make changes to them).
After you have submitted your file in Canvas you will need to present your solution to a supervisor with the entire group. This presentation will take about 20 minutes and takes place during one of the supervision sessions, see TimeEdit. All members of the group must be present during the presentation, and are required to understand all of the code submitted, and it is the responsibility of all of the group members to make sure that this is the case. If one of the group members clearly does not understand the code then the group will not be passed, and will be expected to try again when all members of the group are able to explain the solution.