Class Notes Week 13


Topics

Monday Wednesday Friday


Overview

We will continue to build on our understanding of object-oriented programming, including defining classes. Our exercises this week will have us revisit old assignments/exercises to compare and contrast OOP to an imperative style

Team class

As a reminder, here are the instructions from last week:

Implement a class called Team that stores and manipulates information about a competitive team (e.g., soccer, debate, baseball). Each team keeps track of its name, year, city, wins, and losses. In addition:

Constructor

Every piece of data needs to be initialized in the constructor. How it is initialized is up to the programmer (you!). It must meet specifications; aside from that, it is a design choice to either minimize the number of parameters for easy of use or allow more flexibility by including more parameters.

In this example, our specification explicitly states what parameters to take in - name, year, and city.

def __init__(self, teamname, teamcity, teamyear):
  self.name = teamname
  self.year = teamcity
  self.city = teamyear
  self.wins = 0
  self.losses = 0

Notice that wins and losses must be defined (and the instructions said they are initially 0). Now, let’s add some test code at the bottom of our class file:

if __name__ == "__main__":
  ncaab = Team("Wolverines",2018,"Ann Arbor")

Run the program to see if it works. Our Team object doesn’t have any other methods, so it is hard to tell if it is working.

Exercise: implement and test toString() and getters

Implement toString() and add a test:

  print(ncaab.toString())

This should output 2018 Wolverines: 12 wins, 0 losses. Add get methods for all 5 data members.

Exercise: implement and test setters

Implement wonGame(), lostGame(), and getWinPercent() and add a test for each one*.

Object-oriented Blackjack

In Week 7, we learned to use top-down design to implement a version of blackjack. Today, we will return to that exercise and use object-oriented programming to represent a deck of cards. We will define the following classes:

  1. Card - a Card instance has a rank and suit and represents one playing card
  2. Deck - an instance is a deck of playing cards, initially with 52 cards. The deck has an ability to shuffle its cards and deal a card.
  3. ‘BlackjackHand’ - an instance represents one players entire hand of cards

Card

Let us first design the Card class. I added some bells and whistles that you don’t need to memorize. But I’ve created two lists outside the class that represent all legal values for suits and ranks. This is how we define constants - values that are not part of an instance. Other examples of this include math.pi which is a constant value stored in math library. One could get the legal suits/ranks by importing:

>>> import card
>>> card.legalSuits
['S', 'D', 'H', 'C']
>>> card.legalRanks
['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K']

Another extra feature is the idea of exceptions. When we ask a user for input, we error check it and reprompt them for a value if they were wrong. What should we do if a programmer uses our function incorrectly? We can’t ask the user for a response, since they are not responsible for how the program was written. Instead, we raise exceptions, or errors, that tell the program there is a bug in the program.

Specifications

A Card has a suit and rank, and these should be initialized via parameters in the constructor. In addition, you will need:

Solution to Card example

Here is my solution to the problem (copy and paste parts that you did not get correct or finish). I changed my approach slightly - I made toString() do the work of longString above since that is more useful:

First, your constructor and getters are straightforward:

def __init__(self, rank, suit):
    #A ValueError signifies that the programmer did
    #  not user the function correctly
    if rank not in legalRanks or suit not in legalSuits:
        raise ValueError("Illegal card value")

    self.rank = rank
    self.suit = suit

    def getRank(self):
        return self.rank

    def getSuit(self):
        return self.suit

Next, let us look at the toString() method:

def toString(self):
    """string representation of card in format "Rank of Suit" where
    Rank is a number of {King, Queen, Jack, Ace} and Suit is
    one of Spades, Clubs, Diamonds, Hearts"""

    #Set non-abbreviated version of suit
    if self.suit == "S":
        longsuit = "Spades"
    elif self.suit == "C":
        longsuit = "Clubs"
    elif self.suit == "D":
        longsuit = "Diamonds"
    else:
        longsuit = "Hearts"

    #set non-abbreviated version of rank
    if self.rank == "A":
        longrank = "Ace"
    elif self.rank == "K":
        longrank = "King"
    elif self.rank == "Q":
        longrank = "Queen"
    elif self.rank == "J":
        longrank = "Jack"
    else:
        longrank = self.rank #keep number as is

    return "%s of %s" % (longrank, longsuit)

It’s a bit long, and there are more efficient ways of doing this. But it accomplishes the goal of converting the card into a useful string representation.

Lastly, we have getValue(). For digit ranks (i.e., "2" through "10"), we can just do int() type conversion. This will cause an error, however, on face cards and the Ace. So we’ll handle those as special cases:

def getValue(self):
    """returns value of card for blackjack as an integer.  Ace is 1,
       face cards are 10, and numeric cards are their face value"""

    if self.rank in ["K","Q","J"]:
        return 10
    elif self.rank == "A":
        return 1
    else:
        return int(self.rank) #must be digit if not A,K,Q, or J

Testing this code is essential, and please get in the habit of testing each method as you develop. Here is a test I created that creates a list of 5 cards and tests out all getter methods:

#test card methods
cardList = [Card("A","C")]
cardList.append(Card("K","D"))
cardList.append(Card("Q","H"))
cardList.append(Card("J","C"))
cardList.append(Card("5","D"))

trueValues = [1,10,10,10,5] #I expect these values for each card

#test all methods for each card
for i in range(len(cardList)):
    print("Card %d: %s" % (i,cardList[i].toString()))
    print("Suit:", cardList[i].getSuit())
    print("Rank:", cardList[i].getRank())
    #assert card values
    assert(cardList[i].getValue() == trueValues[i])
    print("Assertion succeeded")
    print()

Notice the use of the assert statement when checking the card value. We could have done this for the other getters as well; use all the tools you have to make testing useful to you.

Deck of cards

Once the Card class has been implemented and thoroughly tested, implement Deck class, which uses instances of the Card class. Your class should:

def toString(self):
    result = ""
    for i in range(len(self.cards)):
        if i%13 == 0:
            result += "\n"
        result += self.cards[i].toSimpleString() + " "
    return result

Test it all out

Here is some test code:

print("Creating a deck of cards...")
deck = Deck()

print("New deck: ")
print(deck.toString())
print()

for i in range(4):
    card = deck.dealCard()
    print("Dealt card:", card.toString())
print("Current length:", deck.getLength())

print()
print("Remaining cards in the deck:")
print(deck.toString())

and sample output:

Creating a deck of cards...
New deck:

4S 3C 6D KC 4D 3H 9S 7D 8H 10C JD 8S 4C
10H 7S KD 5H 5C 10D 5D 3S 8C AC 7C 10S QD
AS 6H QH JC AH JS 2H 6S 4H JH 6C 5S 2S
KS QS AD 3D 2C 2D QC 9H 8D 9D KH 9C 7H

Dealt card: 7 of Hearts
Dealt card: 9 of Clubs
Dealt card: King of Hearts
Dealt card: 9 of Diamonds
Current length: 48

Remaining cards in the deck:

4S 3C 6D KC 4D 3H 9S 7D 8H 10C JD 8S 4C
10H 7S KD 5H 5C 10D 5D 3S 8C AC 7C 10S QD
AS 6H QH JC AH JS 2H 6S 4H JH 6C 5S 2S
KS QS AD 3D 2C 2D QC 9H 8D

Blackjack Hand

Now that we have the essential pieces for a deck of cards, we need to represent each player’s hand (i.e., the cards they have drawn) as a separate class. This class will keep track of the name of the player (e.g., “Jack” or “Dealer”) as well as all cards they have drawn, which is initially empty. In blackjackHand.py, you complete the implementation of the BlackjackHand class. The functions have been stubbed out for you and some initial tests have been provided:

Here is a sample output of the given tests once you complete your implementation (your results will differ due to randomness):

$ python3 blackjackHand.py

test1's hand contains 2 cards
  Ace of Clubs
  Ace of Diamonds
Blackjack value: 2


test2's hand contains 3 cards
  10 of Hearts
  9 of Diamonds
  Ace of Diamonds
Blackjack value: 20

Main program

We will conclude by bringing these classes together to recreate our blackjack solution from early in the semester.

Exercise - review imperative solution

Open the solution from week 7:

$ atom ~/cs21/inclass/w07-design/blackjack_soln.py

With your neighbor, identify all aspects of the program that can now be replaced by functionality of the three classes we just wrote. How would the code change? Once we review the changes, open blackjack_oop.py and see how our solution is improved with objects.