Introduction to Functional Programming – Lab 2: “BlackJack” TDA555 / DIT440, LP1 2016 Home | Schedule | Labs | Lectures | Exercises | Exam | About | FAQ Fire | Forum | TimeEdit | YouTube | Links
 Introduction to Functional Programming – Lab 2: “BlackJack” TDA555 / DIT440, LP1 2016 Home | Schedule | Labs | Lectures | Exercises | Exam | About | FAQ Fire | Forum | TimeEdit | YouTube | Links

Some notes:

• Remember that you have to work in groups of 3. Only groups of size 2 or 3 are allowed to submit! Submissions by only 1 person or more than 3 persons will not be accepted by the Fire system.

Good luck!

In this lab assignment, you will write an implementation of (a simple variant of) the game Black Jack.1 By doing so you will learn to define recursive functions and QuickCheck properties. Since you have not really learned how to do input/output yet, we provide you with a wrapper which takes care of those things for you.

The lab assignment is divided into 2 parts:

• Part I: Task A (writing out what happens when executing the size function) and parts of Task B (implement the functions value, gameOver, and winner).
• Part II: The rest of the assignment (Tasks C - H).

There are also some extra assignments mentioned after Task H. These are not obligatory, but you will learn more when you do them!

This lab assignment has 3 deadlines (2016):

• First deadline (Monday, Sept. 12 at 12:00): Part I as stated above.
• Second deadline (Monday, Sept. 19 at 12:00): The whole lab assignment (Parts I and II).
• Final deadline (Wednesday, October 12 at 23:59): the system will not accept sumissions after this date.

The game

The game you will implement is a simple variant of Black Jack. This section describes the rules that you should implement. We expect your implementation to follow these rules.

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 e.g. 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.

The winner is the player with the highest score that does not exceed 21. If the players end up with the same score, then the bank wins. The bank also wins if both players go bust.

Recursive data types

For this lab, you need the two files Cards.hs and Wrapper.hs. You do not need to understand anything about the wrapper, but you have to understand most of Cards. That module contains some definitions from the lectures; data types and Arbitrary instances for playing cards. See the slides here and here. Read through the file so that you know which types you are supposed to use.

We can define an example hand, consisting of the cards 2 of hearts and jack of spades, as follows:

hand2 = Add (Card (Numeric 2) Hearts)
(Add (Card Jack Spades) Empty)

Cards.hs contains a function that computes the number of cards in a hand:

size :: Num a => Hand -> a
size Empty           = 0
size (Add card hand) = 1 + size hand

In order to understand this function, take some time to work with it before you continue. You can for instance execute size hand2 by hand, on paper. The result should be 2, right?

 Task A. Execute the expression size hand2 by hand, step by step: size hand2 = size (Add (Card (Numeric 2) Hearts) (Add (Card Jack Spades) Empty)) = ... = 2 Write this sequence of equations as comments in a file called BlackJack.hs. This is the file you will submit.

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:

1. They serve as a specification before you write your functions.
3. 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.

Documentation

The code has to be documented. See the Cards.hs and Wrapper.hs 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.

Functions

Write all code in a file called BlackJack.hs. To make everything work, add the following lines at the top of the file:

module BlackJack where

import Cards
import Wrapper

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 Wrapper. Download Cards.hs and Wrapper.hs and store them in the same directory as BlackJack.hs, but do not modify the files.

As usual, 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 G. 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. If it cannot find QuickCheck then you probably installed the minimal version of the Haskell platform which comes without without QuickCheck. Install the full version instead, or if bandwidth is an issue, type cabal install QuickCheck in a command window.

When working on the functions below, it is a good idea to make two definitions,

aCard1 :: Card
aCard1 = ... -- define your favorite card here

aCard2 :: Card
aCard2 = ... -- define another card here

so that you can use these cards to test your functions. Also, make a definition,

aHand :: Hand
aHand = ... -- a Hand with two Cards, aCard1 and aCard2

to test your functions that work on hands.

 Task B. Implement the following functions. Implement a function that, given a hand, calculates the value of the hand according to the rules given above: value :: Hand -> Integer Remember that the special rules for this lab state that all aces have the same value: they are either all 1 or all 11 depending on the total value of the hand. Hint: To write the value function, start by writing a function valueRank :: Rank -> Integer, that calculates the value of a Rank (think about what to do for an ace!), and perhaps also a function valueCard :: Card -> Integer, that calculates the value of a Card. Furthermore, it is a good idea to define and use a function numberOfAces :: Hand -> Integer, that calculates the number of aces in a given hand. Write these three functions first before you start defining the function value. Implement a function that, given a hand, checks if the player is bust. gameOver :: Hand -> Bool Implement a function that, given one hand for the guest and one for the bank (in that order), checks which player has won. winner :: Hand -> Hand -> Player Here Player is a new data type, defined in Wrapper.hs: data Player = Guest | Bank deriving (Show, Eq)

The remaining tasks belong to Part II of the lab.

 Task C. Given two hands, <+ puts the first one on top of the second one: (<+) :: Hand -> Hand -> Hand (Note that a function name with only symbols indicates an infix operator. It is used just like + or -, with the operator between its arguments: h1 <+ h2.) This function must satisfy the following QuickCheck properties. The function should be associative: prop_onTopOf_assoc :: Hand -> Hand -> Hand -> Bool prop_onTopOf_assoc p1 p2 p3 = p1 <+ (p2 <+ p3) == (p1 <+ p2) <+ p3 Furthermore the size of the combined hand should be the sum of the sizes of the two individual hands: prop_size_onTopOf :: Hand -> Hand -> Bool The implementation of this property is not given here, you have to write it yourselves.

 Task D. You also need to define a function that returns a full deck of cards: fullDeck :: Hand 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 using <+.

 Task E. 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 :: Hand -> Hand -> (Hand, 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 F. Given a deck, play for the bank according to the rules above (starting with an empty hand), and return the bank’s final hand: playBank :: Hand -> Hand To write this function you will probably need to introduce a helper 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' deck bankHand ... ... where (deck', bankHand') = draw deck bankHand You can read more about where here.

 Task G. Make sure you have read about random numbers before starting this task. Given an StdGen and a hand of cards, shuffle the cards and return the shuffled hand: shuffle :: StdGen -> Hand -> Hand Note: See above about how to import QuickCheck without getting name clashes with the shuffle function. If you want a (small) challenge, try to implement shuffle without reading the following hints. 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: prop_shuffle_sameCards :: StdGen -> Card -> Hand -> Bool prop_shuffle_sameCards g c h = c belongsTo h == c belongsTo shuffle g h For this we need the helper function belongsTo, which returns True iff. the card is in the hand. belongsTo :: Card -> Hand -> Bool c belongsTo Empty = False c belongsTo Add c' h = c == c' || c belongsTo h (Here we’re using  to 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

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 H. To “package up” these functions, write the following code: implementation = Interface { iEmpty = Empty , iFullDeck = fullDeck , iValue = value , 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.

Happy playing!

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 Wrapper.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 assignment, use Haskell comments to indicate what part of the file contains the answer to the assignment. For answers in natural language, you can use Swedish or English; write these answers also as Haskell comments.

Before you submit your code, clean it up! Remember, submitting clean code is Really Important, and simply the polite thing to do. After you feel you are done, spend some time on cleaning your code; make it simpler, remove unneccessary things, etc. We will reject your solution if it is not clean. Clean code:

• Does not have long lines (< 78 characters)
• Has a consistent layout (do not use TAB characters in your code)
• Has type signatures for all top-level functions
• Has no junk (junk is unused code, commented code, unneccessary comments)
• Has no overly complicated function definitions
• Does not contain any repetitive code (copy-and-paste programming)

Please do not submit the modules Cards.hs and Wrapper.hs` (unless an extra assignment required you to make changes to them).

When you are done, please submit it using the Fire system.

Once you have submitted, please arrange to present your solution to one of the course assistants according to the instructions on the lab overview page.

Good luck!

1. This lab is based on material by Nils Anders Danielsson, Koen Claessen, Hampus Ram, Andreas Farre, Sebastian Sylvan and Mary Bergman.