CSC108H Assignment 3, Summer 2012
Part 1 Due 2012-08-11 23:55
Part 2 Due 2012-08-13 23:55
Introduction
The purpose of this assignment is to give you practice with classes, testing, and with raw input to get information from the user. The focus is not on inheritance, but rather in how classes allow us chunk problems into simpler bits of code.
You should spend at least one hour
reading through this handout to make sure you understand what is
required, so that you don't waste your time going in circles, or worse yet,
hand in something you think is correct and find out later that you
misunderstood the specifications.
Highlight the handout, scribble on it, and find all the things
you're not sure about as soon as possible.
It is probably worth spending at least a day thinking about the assignment before writing code of any sort. Try to think through potential problems to catch them before they appear. This will save hours of frustrating debugging time.
Overview
You've been tasked with writing a very basic blackjack game, along with some test for the parts of the game that don't require direct input from the user. This assignment has two parts - writing the test cases, and writing the blackjack game itself.
Blackjack
Blackjack is a simple betting card game played with a standard deck. A card in a standard deck has a suit and a value. The suit may be one of the four following options: Club, Diamond, Heart, Spade. The value may be one of the following 13 options: Ace, 2, 3, 4, 5, 6, 7, 8, 9, 10, Jack, Queen, King. We will implement a very simple version of blackjack with one player and one dealer. They will each start with some non-0 amount of money specified by the user. The amount of money can be different for the player and the dealer. These two players will play rounds of blackjack until either the player or dealer have no cash left or the player decides to finish the game. At least one round of blackjack will be played.
A round of blackjack proceeds as follows. First, the player decides how much money they are willing to bet this round. Then, this amount of money is added put into a 'pot' by both the dealer and the player. Two cards are then dealt to the dealer and the player. The player's two cards are revealed. The player is then free to hit until either the score of their hand is over 21 or they decides to stay. A hit by the player means that they are dealt one more card, and their hand size is increased by 1. A stay means the player is happy with their hand and does not want any more cards. A player is free to stay with their two initial cards, and can hit as many times as they like. When the player has stayed, the two cards of the dealer are revealed. The dealer is then free to hit/stay in the same way as the player. When the dealer has finished, the pot goes to whoever has the highest scoring hand whose score does not exceed 21. If both the dealer and the player have the same score, then the pot is split between them.
The score of a blackjack hand is calculated as follows: 10 for each face(Jack, Queen, King) card in the hand. Cards with number values are worth the number on the card. (So a 2 of clubs is worth 2, a 5 of diamonds is worth 5, and so on). Aces are worth either 1, or 11, depending on what gets the score closest to 21 without going over. If the hand of a player or a dealer goes above 21, then the player or dealer is said to bust. The suit of a card plays no role in blackjack.
Your task
This assignment includes two parts - one is writing the code, the other is writing the test cases. Because the point of the assignment is to get comfortable with using classes, a lot of the class structure is enforced. First download the following files: cards.py and blackjack.py. You will note that each of these contains several classes, with several method stubs. Your job is to complete the method stubs. You may write additional helper functions/methods if you find it useful. You should not change the order of the classes. You are to update the docstrings/change the variable names. We will go through these by file and by class.
First we look at cards.py. This file contains three classes: Card, Deck and BlackjackHand.
Card is a class we define to represent playing cards. It has two instance variables, value which is a str that contains the value of the Card object, and suit which is a str that contains the suit of the Card object. You need to implement the following methods:
- __init__(self, value, suit) (self, str, str)->(Card): This constructor creates a Card object whose value is value and suit is suit.
- __str__(self) (self) -> str: This method returns a string in the format 'value of suit', where value and suit are the instance variables of the Card object.
Note that we don't put any restrictions on the values and suits of the card. These restrictions are made in the Deck Class. Conceivably this would allow us to use our card class for other decks that have different types of cards. Our Deck class represents a regular deck of playing cards. You should have an instance variable that is a list of Card objects to represent these cards. You may find it useful to have other instance variables as well. You need to implement the following methods:
- __init__(self) (self) -> Deck: This constructor creates a Deck object that represents a regular 52-card deck of playing cards.
- deal(self, num_cards) (self, int) -> list of Card: This will return a list of num_cards Cards from the deck without replacement. This means that if, for eg. the Ace of Diamonds has been given out, it cannot be given out again until the Ace of Diamonds is shuffled back into the deck. If num_cards is greater than the number of cards remaining in the deck, as many cards as possible should be dealt, and the remaining list positions should be filled with
None
.
- contains(self, card) (self, Card) -> bool: Return True iff card is still in the deck and has not been dealt out.
- contained(self, card) (self, Card) -> bool: Return True iff card was once in the deck but is currently dealt out.
- len(self) (self) -> int: Return the number of cards in the deck that are not dealt out.
- shuffle(self) (self) -> NoneType: Shuffles the cards that have not been dealt out.
- reshuffle(self) (self) -> NoneType: Takes all the cards that have been dealt out and returns them to the deck. Then shuffles the entire deck.
The last class in cards.py is BlackjackHand. This class represents a hand of blackjack. This hand may belong to a player or a dealer. It also contains cards, but does so in a very different way than a deck. You need to implement the following methods:
- __init__(self, cards) (self, list of Card)-> BlackjackHand: This constructor takes a list of Cards cards that contains two Card objects and creates a BlackjackHand object that represents a two card blackjack hand.
- add_card(self, card) (self, Card) -> NoneType: Add the Card card to the blackjack hand.
- score(self) (self) -> int: Returns the score of the blackjack hand.
- __str__(self) (self) -> str: Returns a string that contains several lines. Each line contains the string representation of one Card in the hand.
blackjack.py contains more complicated classes. These classes are: BlackjackPlayer, BlackjackDealer, BlackjackGame.
BlackjackPlayer is a class that represents the player playing the game. This means that a lot of the functions will ask for user input. You are required to ensure that the input obtained from the user is legal. It must have an instance variable cash that represents how much money the player has. It must have an instance variable hand to represent the player's hand. You need to implement the following methods:
- __init__(self, cash) (self, int) -> BlackjackPlayer: This constructor creates a BlackjackPlayer object with an empty hand, and an amount of money equal to cash.
- bet(self, max_bet) (self, int)-> int: This method first displays max_bet which is the maximum legal bet. It then displays the amount of money the player has on hand. It then asks the user how much they wish to bet, repeating the query until a legal answer is given. This value is subtracted from the players cash and is returned. Bets must be greater than 0.
- hit(self) (self) -> bool: This method should first display to the user what the cards in their hand are. It then displays the score of the user's hand. Finally it asks if they user wants to hit or stay. Returns True iff the user wants to hit.
BlackjackDealer is a complementary class to BlackjackPlayer. This class represents the dealer. It differs from the player in that the dealer is not a human player. None of the methods in this class should require user input. It also differs from the player in that a BlackjackDealer contains a deck that is used to deal the cards. Note that in practice, blackjack dealers often draw their cards from several decks to prevent card counting. To somewhat model this, we allow a BlackjackDealer to use several decks to draw cards from. This class must have an instance variable cash that represents how much money the dealer has. It must have an instance variable hand to represent the dealer's hand.You need to implement the following methods:
- __init__(self, cash, num_decks) (self, int, int): This constructor creates a BlackjackDealer object with an empty hand, an amount of money equal to cash, and with a deck of cards made from num_decks standard card decks. You may assume num_decks is greater than 0.
- deal_hand(self, num_cards) (self, int) -> list of Card: Causes the dealer to deal num_cards from their deck. These cards are returned as a list of Card objects.
- match_bet(self, bet) (self, int) -> int: Should subtract bet from the dealer's cash and return it. Should never be called with a value of bet that is more than the dealer's cash.
- play(self, player_hand) (self, BlackjackHand) -> int: This should show the value of the dealer's hand to the player. If the player cheated (more on this in additional requirements), return -1 and show a message indicating that the player cheated. If the player busted, return -1, and show a message that indicates the contents of the dealer's hand, and the player's hand. Otherwise, the dealer should hit until they either bust, or exceed or tie the players hand. They should show the result of each hit as it happens. Finally, if the dealer exceeds the player's hand, return -1. If the dealer busts, return 1. If there is a tie, return 0. In all cases there should be a message to the user stating what happened, and displaying the final hand of the player and the dealer.
- reshuffle(self, max_remaining) (self, int) -> NoneType: If the amount of remaining cards in the dealer's deck is less than max_remaining, then all of the cards that have been dealt are returned to the deck, and the deck is shuffled.
The final class is called BlackjackGame. This class should control the interactions between the player and the dealer. Of particular importance is the instance variable which represents the literal pot. The code skeleton given is structured in such a way that money should never go directly from the player to the dealer or vice versa. Money should go from the player and dealer into the pot, and then back to the dealer or player. Your task is to complete the following methods:
- __init__(self, dealer, player) (self, BlackjackDealer, BlackjackPlayer)-> BlackjackGame: The code for this method is complete, you simply need to complete the docstring.
- play(self) (self) -> NoneType: This method controls the play of the game. It controls the execution of each round of a blackjack game. One round must be played. The dealer's deck should be shuffled before the first round. Each round consists of asking the player how much they want to bet, and then collecting that amount from the player and dealer and putting it in the pot. Then the dealer deals two cards to the player and the dealer. Then the player is allowed to hit as long as they wish (until they bust). Each time the player hits, the dealer deals one card for the player. Once this is complete, the dealer is allowed to play. Once this has finished the game should determine who won the round, and move the money from the pot into the winner's cash. Finally, the player should be told how much money they have, and asked if they wish to continue. If so, a new round should begin (unless the player or dealer have no cash left). If the dealer has less than 22 cards when a round begins, the dealer should have their cards returned and reshuffled. The user should receive a message that this is happening. If not, the game should end, and the player should be informed of how much cash they have.
This assignment has two parts. For the second part, you need to complete the methods listed above. For the first part you need to create test files that use nose for Deck, BlackjackHand, and BlackjackDealer. These should be submitted in a zip archive called tests.zip
. Each test file should have the word test as part of it's filename. You do not need write tests for Card as it is too simple to create meaningful tests for. You do not need to create tests for BlackjackPlayer and BlackjackGame as these classes require user input to test. Your tests should not require user input. You will note that the tests are due before the assignment. This is to think about tests early. We will run your tests against your code.
Additional requirements
You must not use any break
or continue
statements.
Any functions that do will receive a mark of zero.
We are imposing this restriction (and we have not even taught you these
statements)
because they are very easy to "abuse," resulting in terrible code.
In the 'hit' phase, a player could cheat and alter the cards in his hand. For example consider cheater.py. We could create a BlackjackGame using a Cheater object, since Cheater is a subclass of BlackjackPlayer (This means that every cheater is a BlackjackPlayer). BlackjackDealer.play should be robust enough that it should detect cheating of this form, where a player creates cards of their own.
Clarification
- The list of methods we give is not exhaustive. This assignment will be easier if you write some helper methods and functions.
- The messages that you need to give to the user are not specified exactly. This is because we will be running your code. As long as it is clear what is going on, that's fine. However, this is not very specific. To give you something specific, we give you an example transcript of someone running the program here.
- You need to write docstrings for the classes as well as the methods.
- Anytime user input is mentioned, this should be implemented with the
raw_input
function.
How to tackle this assignment
Here is our suggestion. First download the code from cards.py and blackjack.py. Some of the more simple Classes like Card can be finished immediately. Beyond that, you should break up the larger more complicated methods into chunks. Imagine how you would solve the problem with real world objects. Then transfer those ideas to virtual objects. One of the advantages of using Objects, is that they are a powerful abstraction that allows us to apply 'real world' logic to computer problems. Finally, make sure you write the tests before writing the code. Thinking about how to break classes, and how the classes behave in corner cases should allow you to develop a good intuition for how they are meant to be used.
Principles:
-
To avoid getting overwhelmed, deal with one function at a time.
Start with functions that don't call any other functions; this will allow
you to test them right away.
-
For each function that you write, plan test cases for that function and
actually write the Python code to implement those tests before you
write the function.
It is hard to have the discipline to do this, but you will have a huge
advantage over your peers if you do.
-
Keep in mind throughout that any function you have might be a useful helper
for another function. Part of your marks will be for taking advantage of opportunities to call an existing
function.
-
As you write each function, begin by designing it in English, using only a few
sentences. If your design is longer than that, shorten it by describing the
steps at a higher level that leaves out some of the details.
When you translate your design into Python, look for steps that are described at such
a high level that they don't translate directly into Python.
Design a helper function for each of these, and put a call to the helpers into
your code.
Don't forget to write a docstring for each helper!
Marking
These are the aspects of your work that we will
focus on in the marking:
-
Correctness: Your code should perform as specified.
Correctness, as measured by our tests and running the program, will count for the
largest single portion of your marks.
-
Docstrings: For each function that you design from scratch,
write a good
docstring
.
(Do not change the docstrings that we have already written for you.)
Make sure that you read the Assignment rules page for some important
rules and guidelines about docstrings.
-
Internal comments: Within
functions, the more complicated parts of your code should also be
described using "internal" comments.
-
Programming style: Your variable names should be meaningful and
your code as simple and clear as possible.
-
Good use of helper functions: If you find yourself repeating a task, you should add
a helper function and call that function instead of duplicating the
code.
And if a function is more than about 20 lines long, consider introducing
helper functions to do some of the work -- even if they will only be called once.
-
Formatting style: Make sure that you read the Assignment rules page for some important
rules and guidelines about formatting your code.
Submit a file named tests.zip to markus for the first part. For the second, submit two files - one named cards.py, and another named blackjack.py. Once you have submitted, be sure to check that you have submitted the correct version; new or missing files will not be accepted after the due date.
If you wish to work with a partner, one of you needs to invite the other
to be a partner, and then they need to accept the invitation.
To invite a partner, navigate to the Assignment 2 page,
find "Group Information", and click on "Invite".
You will be prompted for the other student's cdf user name; enter it.
To accept an invitation, find "Group Information" on the Assignment 2 page,
find the invitation listed there, and click on "Join".
Note that, when working in a pair, only one person should submit the assignment.
Remember that spelling of filenames, including case, counts.
If your file is not named exactly as above,
your code will receive zero for correctness.