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
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:
wonGame
that increments the team’s number of wins by 1.lostGame
that updates the number of losses.getWinPercent
that returns the percentage of games won out of the total number of games played. If a team has played 0 games, return a winning percentage of 0.0. If a team has won 3 out of 4 games, return a winning percentage of 0.75Every 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:
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.
toString()
and gettersImplement toString()
and add a test:
This should output 2018 Wolverines: 12 wins, 0 losses
. Add get methods for all 5 data members.
Implement wonGame()
, lostGame()
, and getWinPercent()
and add a test for each one*.
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:
Card
- a Card
instance has a rank and suit and represents one playing cardDeck
- 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.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.
A Card
has a suit and rank, and these should be initialized via parameters in the constructor. In addition, you will need:
toString()
method that returns that returns a verbose representation of the string e.g., “Ace of Diamonds” or “10 of Clubs”.shortString()
method to return the raw values (abbreviated) of suit and rank e.g., “10C” for rank 10 and suit C.getValue()
method that returns an integer value for the card. If the card rank is numeric, send that value as an integer. Otherwise, send 1
for "A"
and 10
for "K"
, "Q"
, or "J"
.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.
Once the Card
class has been implemented and thoroughly tested, implement Deck
class, which uses instances of the Card
class. Your class should:
Card
class. Use the lists legalSuits
and legalRanks
to enumerate all 52 cards. (Hint: you may want to simply call some of the methods below to a) create 52 cards and b) shuffle them).regenerate
method that reinitializes the deck to 52 cards.shuffle
method that shuffles the list of cards by calling random.shuffle(list)
getLength()
method that returns the number of cards in the deck.toString()
method that prints out the contents of the deck. You may want to do something like this to print out the list (which I called self.cards
):def toString(self):
result = ""
for i in range(len(self.cards)):
if i%13 == 0:
result += "\n"
result += self.cards[i].toSimpleString() + " "
return result
dealCard()
that returns one card from the deck. Recall that the pop()
method removes the last item in the deck and returns the value that was removed. Be sure to first check if there are any cards to deal; if not, call regenerate
to recreate the deck.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
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:
toString()
should return the number of cards in the hand, each card in the hand (hint: use the Card.toString()
method), and the total value of the hand.getName()
to return the user’s namegetScore()
accumulates the value of each card and returns the sum (int)getLength()
returns the number of cards in the handHere 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
We will conclude by bringing these classes together to recreate our blackjack solution from early in the semester.
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.