CS 111
Spring 2018

Old version

This is the CS 111 site as it appeared on May 10, 2018.

Lab 10: Object-oriented programming, part II

Task 0: complete a class for Card objects

To implement a card game, one type of object that would be useful is an object that represents an individual card from a deck of cards.

  1. Begin by downloading the following file: card.py

    Open it in IDLE. You’ll see that we’ve begun to define a class called Card that will serve as a blueprint for a single playing card.

    Each Card object has two attributes:

    • a rank, which is either an int (for numeric cards) or a single upper-case letter (for Aces and for face cards like Queens)
    • a suit, which is stored as a single upper-case letter ('C' for Clubs, 'S' for Spades, 'H' for Hearts, and 'D' for Diamonds)

    For example, here’s one way to picture what a Card object representing the 4 of Clubs would look like in memory:

    +----------------+
    |        +-----+ |
    |   rank |  4  | |
    |        +-----+ |
    |        +-----+ |
    |   suit | 'C' | |
    |        +-----+ |
    +----------------+
    

    And here’s a picture of a Card object representing the Queen of Diamonds:

    +----------------+
    |        +-----+ |
    |   rank | 'Q' | |
    |        +-----+ |
    |        +-----+ |
    |   suit | 'D' | |
    |        +-----+ |
    +----------------+
    
  2. Locate the constructor that we’ve provided for Card objects.

    Notice how it uses the type() function to determine if the value passed in for rank is an int. If it is, it simply stores that integer value in the rank attribute (self.rank). Otherwise, it assumes that the rank is a string, and it uses string indexing and the upper() method to ensure that the value stored in the suit attribute is a single upper-case character.

    Run card.py in IDLE, and enter these statements in the Shell:

    >>> c1 = Card(4, 'C')
    >>> c1.rank
    4
    >>> c1.suit
    'C'
    >>> c2 = Card('queen', 'diamonds')
    >>> c2.rank
    'Q'
    >>> c2.suit
    'diamonds'
    

    Note that the constructor converted the rank provided for c2 (the string 'queen') to the single upper-case letter 'Q'. However, the constructor did not convert the suit 'diamonds' to a single upper-case letter. It simply stored the string provided for suit without changing it.

    Modify the constructor so that it ensures that the value stored in the suit attribute is a single upper-case letter. You may assume that the value passed in for suit is a string of one or more letters.

    Once you have made the necessary changes, re-run card.py and test the new version:

    >>> c2 = Card('queen', 'diamonds')
    >>> c2.suit
    'D'
    
  3. We’ve also given you an initial version of a __repr__ method for converting a Card object to a string. It simply constructs a string consisting of the rank and suit, separated by a space:

    >>> c2 = Card('Q', 'D')
    >>> c2
    Q D
    

    Although it makes sense to only store a single character for a Card object’s rank and suit, we’d like the string representation of the of the object to be more descriptive.

    Begin by copying the following lines into card.py, putting them near the top of the file, before the header for the Card class:

    RANK_NAMES = {'A': 'Ace', 'J': 'Jack', 'Q': 'Queen', 'K': 'King'}
    SUIT_NAMES = {'C': 'Clubs', 'S': 'Spades', 'H': 'Hearts', 'D': 'Diamonds'}
    

    Since we’re putting these lines in the global scope, the variables RANK_NAMES and SUIT_NAMES are global variables (variables that can be accessed from anywhere in the file), and we’re capitalizing their names to indicate that fact.

    Both of these variables represent dictionaries:

    • RANK_NAMES is a dictionary that connects each non-numeric rank to a more descriptive string for that rank. For example, it connects the rank 'A' to the string 'Ace'.

    • SUIT_NAMES is a dictionary that connects each suit character to a more descriptive string for that suit. For example, it connects the suit 'C' to the string 'Clubs'.

    Modify the __repr__ method so that it uses these dictionaries to create and return a string of the form 'rank-name of suit-name'. For example:

    >>> c2 = Card('Q', 'D')
    >>> c2
    Queen of Diamonds
    

    If the called object’s rank is of type int, you should simply convert it to a string, as we do in the initial __repr__. Otherwise, you should use the RANK_NAMES dictionary to look up the descriptive name associated with the object’s rank.

    Similarly, you should use the SUIT_NAMES dictionary to look up the descriptive name associated with the object’s suit.

  4. Examine the method called get_value() in card.py. It returns the value of the called Card object. More specifically:

    • if a Card object has a numeric rank, it returns the rank
    • otherwise, it returns a value of 10

    This follows the common convention of giving all face cards a value of 10.

    For example:

    >>> c1 = Card(4, 'C')
    >>> c1.get_value()
    4
    >>> c2 = Card('queen', 'diamonds')
    >>> c2.get_value()
    10
    

    The current version of get_value() also gives Aces a value of 10. Modify the method so that it gives Aces a value of 1 instead:

    >>> c3 = Card('A', 'S')
    >>> c3
    Ace of Spades
    >>> c3.get_value()
    1
    

Task 1: use and complete a class for Hand objects

Next, we’ll consider another type of object that would be useful when implementing a card game.

  1. Begin by downloading the following files:

    Open both files in IDLE.

    In hand.py, we’ve begun to define a class called Hand that will serve as a blueprint for objects that represent a single hand of cards.

    Each Hand object has a single attribute called cards that is initially an empty list. As Card objects are added to the Hand using the add_card() method, they are concentenated to the end of this list.

  2. After reviewing and understanding the Hand class, add client code to lab10_client.py to accomplish the tasks listed below.

    1. Create two Card objects:

      • one for the 7 of Hearts, assigning it to a variable c1
      • one for the Ace of Diamonds, assigning it to a variable c2
    2. Create a single Hand object and assign it to a variable h1.

    3. Use the add_card() method to add both of the Card objects to h1. You will need to call the method twice, once for each Card.

    4. Print h1 as follows:

      print('first hand:', h1)
      

    Run lab10_client.py, and you should see the following output:

    first hand: [7 of Hearts, Ace of Diamonds]
    
  3. As part of the Hand class, add a method called num_cards() that returns the number of cards in the called Hand object (self). Make sure to indent the method under the class header.

    To test it, add the following line to the end of your client code:

    print('number of cards:', h1.num_cards())
    

    Rerun the program, and you should now see:

    first hand: [7 of Hearts, Ace of Diamonds]
    number of cards: 2
    
  4. As part of the Hand class, add a method called get_value() that returns the total value of the cards in the called Hand object (self).

    You will need to perform a cumulative computation, and you should use the Card version of the get_value() method to determine the individual value of each Card object in the Hand.

    To test your new method, add the following line to the end of your client code:

    print('total value:', h1.get_value())
    

    Rerun the program, and you should now see:

    first hand: [7 of Hearts, Ace of Diamonds]
    number of cards: 2
    total value: 8
    
  5. As part of the Hand class, add a method called has_any() that takes a card rank as its only input, and that returns True if there is at least one occurrence of a Card with that rank in the called Hand object (self), and that returns False if there are no occurrences of that rank in the Hand. (Note that the suits of the Card objects don’t matter in this method; we’re only looking at their ranks.)

    To test your new method, add the following lines to the end of your client code:

    print('has at least one 7:', h1.has_any(7))
    print('has at least one Ace:', h1.has_any('A'))
    print('has at least one Queen:', h1.has_any('Q'))
    

    Rerun the program, and you should now see:

    first hand: [7 of Hearts, Ace of Diamonds]
    number of cards: 2
    total value: 8
    has at least one 7: True
    has at least one Ace: True
    has at least one Queen: False
    

Task 2: use inheritance to define a class for Blackjack hands

If we were implementing a game of Blackjack, we would need an object for a hand of cards. Our existing Hand class has most of the functionality that we need, but we’d like the value of a hand to be computed somewhat differently. Rather than defining a completely new class, we’ll take advantage of inheritance to define a subclass of the Hand class.

  1. In hand.py, write a header for a class called BlackjackHand that is a subclass of the Hand class:

    class BlackjackHand(Hand):
    

    The use of (Hand) after the name of the new class causes the new class to inherit all of the attributes and methods of the Hand class.

    Because we’re not adding any new attributes to objects of this class, we don’t even need to write a new constructor!

  2. As discussed above, we’d like the value of a BlackjackHand to be computed somewhat differently than the value of a regular Hand. In particular, if a BlackjackHand has one or more Aces, we want to give one of the Aces a value of 11 unless doing so would lead the hand to have a total value that is greater than 21.

    Override the inherited get_value() method by writing a new version of get_value() that is part of the BlackjackHand class.

    The new method should start by determining the standard value of the hand. You can do so by calling the inherited version of get_value(), and because that inherited method is being overriden, you will need to use super() to access it:

    value = super().get_value()
    

    Once you have this value, determine if the called object has any Aces in it. (Hint: Use one of the other inherited Hand methods.) If it does, determine whether you can afford to count one of the Aces as being worth 11 instead of 1, and adjust the value as needed. Finally, return the value of the hand.

  3. To test your new subclass, cut and paste the following client code into the bottom of your lab10_client.py file:

    print()
    h2 = BlackjackHand()
    h2.add_card(c1)
    h2.add_card(c2)
    print('second hand:', h2)
    print('number of cards:', h2.num_cards())
    print('total value:', h2.get_value())
    print() 
    print('adding a card to h2...')
    c3 = Card(4, 'C')
    h2.add_card(c3)
    print('updated second hand:', h2)
    print('number of cards:', h2.num_cards())
    print('total value:', h2.get_value())
    

    If your new class is working correctly, you’ll see the following new lines of output:

    second hand: [7 of Hearts, Ace of Diamonds]
    number of cards: 2
    total value: 18
    
    adding a card to h2...
    updated second hand: [7 of Hearts, Ace of Diamonds, 4 of Clubs]
    number of cards: 3
    total value: 12
    

    Note that the second hand (which starts out with the same cards as as the first hand), originally has a value of 18, since the Ace is counted as 11. However, once we add a third card to it, its value becomes 12; counting the Ace as 11 would bring the total value of the hand over 21, so we count the Ace as 1 instead.

Task 3: submit your work

You should use Apollo to submit:

Don’t worry if you didn’t finish all of the tasks. You should just submit whatever work you were able to complete during lab.

Here are the steps:

  1. Login to Apollo, using the link in the left-hand navigation bar. You will need to use your Kerberos user name and password.
  2. Check to see that your BU username is at the top of the Apollo page. If it isn’t, click the Log out button and login again.
  3. Find the appropriate lab section on the main page and click Upload files.
  4. For each file that you want to submit, find the matching upload section for the file. Make sure that you use the right section for each file. You may upload any number of files at a time.
  5. Click the Upload button at the bottom of the page.
  6. Review the upload results. If Apollo reports any issues, return to the upload page by clicking the link at the top of the results page, and try the upload again, per Apollo’s advice.
  7. Once all of your files have been successfully uploaded, return to the upload page by clicking the link at the top of the results page. The upload page will show you when you uploaded each file, and it will give you a way to view or download the uploaded file. Click on the link for each file so that you can ensure that you submitted the correct file.