15. Inheritance

15.1. Inheritance

The language feature most often associated with object-oriented programming is inheritance. Inheritance is the ability to define a new class that is a modified version of an existing class.

The primary advantage of this feature is that you can add new methods to a class without modifying the existing class. It is called inheritance because the new class inherits all of the methods of the existing class. Extending this metaphor, the existing class is sometimes called the parent class. The new class may be called the child class or sometimes subclass.

Inheritance is a powerful feature. Some programs that would be complicated without inheritance can be written concisely and simply with it. Also, inheritance can facilitate code reuse, since you can customize the behavior of parent classes without having to modify them. In some cases, the inheritance structure reflects the natural structure of the problem, which makes the program easier to understand.

On the other hand, inheritance can make programs difficult to read. When a method is invoked, it is sometimes not clear where to find its definition. The relevant code may be scattered among several modules. Also, many of the things that can be done using inheritance can be done as elegantly (or more so) without it. If the natural structure of the problem does not lend itself to inheritance, this style of programming can do more harm than good.

In this chapter we will demonstrate the use of inheritance as part of a program that plays the card game Old Maid. One of our goals is to write code that could be reused to implement other card games.

15.2. A hand of cards

For almost any card game, we need to represent a hand of cards. A hand is similar to a deck, of course. Both are made up of a set of cards, and both require operations like adding and removing cards. Also, we might like the ability to shuffle both decks and hands.

A hand is also different from a deck. Depending on the game being played, we might want to perform some operations on hands that don’t make sense for a deck. For example, in poker we might classify a hand (straight, flush, etc.) or compare it with another hand. In bridge, we might want to compute a score for a hand in order to make a bid.

This situation suggests the use of inheritance. If Hand is a subclass of Deck, it will have all the methods of Deck, and new methods can be added.

We add the code in this chapter to our code from the previous chapter. In the class definition, the name of the parent class appears in parentheses:

class Hand(Deck):
    pass

This statement indicates that the new Hand class inherits from the existing Deck class.

The Hand constructor initializes the attributes for the hand, which are name and cards. The string name identifies this hand, probably by the name of the player that holds it. The name is an optional parameter with the empty string as a default value. cards is the list of cards in the hand, initialized to the empty list:

1
class Card:
2
    suits = ["Clubs", "Diamonds", "Hearts", "Spades"]
3
    ranks = ["narf", "Ace", "2", "3", "4", "5", "6", "7",
4
             "8", "9", "10", "Jack", "Queen", "King"]
5
 
6
    def __init__(self, suit=0, rank=0):
7
        self.suit = suit
8
        self.rank = rank
9
 
10
    def __str__(self):
11
        return (self.ranks[self.rank] + " of " + self.suits[self.suit])
12
 
13
    def cmp(self, other):
14
        # Check the suits
15
        if self.suit > other.suit:
16
            return 1
17
        if self.suit < other.suit:
18
            return -1
19
        # Suits are the same... check ranks
20
        if self.rank > other.rank:
21
            return 1
22
        if self.rank < other.rank:
23
            return -1
24
        # Ranks are the same... it's a tie
25
        return 0
26
 
27
    def __eq__(self, other):
28
        return self.cmp(other) == 0
29
 
30
    def __le__(self, other):
31
        return self.cmp(other) <= 0
32
 
33
    def __ge__(self, other):
34
        return self.cmp(other) >= 0
35
 
36
    def __gt__(self, other):
37
        return self.cmp(other) > 0
38
 
39
    def __lt__(self, other):
40
        return self.cmp(other) < 0
41
 
42
    def __ne__(self, other):
43
        return self.cmp(other) != 0
44
 
45
class Deck:
46
 
47
    def __init__(self):
48
        self.cards = []
49
        for suit in range(4):
50
            for rank in range(1, 14):
51
                self.cards.append(Card(suit, rank))
52
 
53
    def __str__(self):
54
        s = ""
55
        for card in self.cards:
56
            s += str(card) + '\n'
57
        return s
58
 
59
    def shuffle(self):
60
        import random
61
        random.shuffle(self.cards)
62
 
63
    def remove(self, card):
64
        if card in self.cards:
65
            self.cards.remove(card)
66
            return True
67
        else:
68
            return False
69
 
70
    def pop(self):
71
        return self.cards.pop()
72
 
73
    def is_empty(self):
74
        return self.cards == []
75
 
76
class Hand(Deck):
77
 
78
    def __init__(self, name=""):
79
        self.cards = []
80
        self.name = name
 

For just about any card game, it is necessary to add and remove cards from the deck. Removing cards is already taken care of, since Hand inherits remove from Deck. But we have to write add. We use the list append method to add the new card to the end of the list of cards.

1
class Card:
2
    suits = ["Clubs", "Diamonds", "Hearts", "Spades"]
3
    ranks = ["narf", "Ace", "2", "3", "4", "5", "6", "7",
4
             "8", "9", "10", "Jack", "Queen", "King"]
5
 
6
    def __init__(self, suit=0, rank=0):
7
        self.suit = suit
8
        self.rank = rank
9
 
10
    def __str__(self):
11
        return (self.ranks[self.rank] + " of " + self.suits[self.suit])
12
 
13
    def cmp(self, other):
14
        # Check the suits
15
        if self.suit > other.suit:
16
            return 1
17
        if self.suit < other.suit:
18
            return -1
19
        # Suits are the same... check ranks
20
        if self.rank > other.rank:
21
            return 1
22
        if self.rank < other.rank:
23
            return -1
24
        # Ranks are the same... it's a tie
25
        return 0
26
 
27
    def __eq__(self, other):
28
        return self.cmp(other) == 0
29
 
30
    def __le__(self, other):
31
        return self.cmp(other) <= 0
32
 
33
    def __ge__(self, other):
34
        return self.cmp(other) >= 0
35
 
36
    def __gt__(self, other):
37
        return self.cmp(other) > 0
38
 
39
    def __lt__(self, other):
40
        return self.cmp(other) < 0
41
 
42
    def __ne__(self, other):
43
        return self.cmp(other) != 0
44
 
45
class Deck:
46
 
47
    def __init__(self):
48
        self.cards = []
49
        for suit in range(4):
50
            for rank in range(1, 14):
51
                self.cards.append(Card(suit, rank))
52
 
53
    def __str__(self):
54
        s = ""
55
        for card in self.cards:
56
            s += str(card) + '\n'
57
        return s
58
 
59
    def shuffle(self):
60
        import random
61
        random.shuffle(self.cards)
62
 
63
    def remove(self, card):
64
        if card in self.cards:
65
            self.cards.remove(card)
66
            return True
67
        else:
68
            return False
69
 
70
    def pop(self):
71
        return self.cards.pop()
72
 
73
    def is_empty(self):
74
        return self.cards == []
75
 
76
class Hand(Deck):
77
 
78
    def __init__(self, name=""):
79
        self.cards = []
80
        self.name = name
81
 
82
    def add(self, card):
83
        self.cards.append(card)
84
 
85
myHand = Hand('Joe')
86
myHand.add(Card(0,3))
87
myHand.add(Card(3,12))
88
print(myHand)
 

15.3. Dealing cards

Now that we have a Hand class, we want to deal cards from the Deck into hands. It is not immediately obvious whether this method should go in the Hand class or in the Deck class, but since it operates on a single deck and (possibly) several hands, it is more natural to put it in Deck.

deal should be fairly general, since different games will have different requirements. We may want to deal out the entire deck at once or add one card to each hand.

deal takes two parameters, a list (or tuple) of hands and the total number of cards to deal. If there are not enough cards in the deck, the method deals out all of the cards and stops:

1
class Card:
2
    suits = ["Clubs", "Diamonds", "Hearts", "Spades"]
3
    ranks = ["narf", "Ace", "2", "3", "4", "5", "6", "7",
4
             "8", "9", "10", "Jack", "Queen", "King"]
5
 
6
    def __init__(self, suit=0, rank=0):
7
        self.suit = suit
8
        self.rank = rank
9
 
10
    def __str__(self):
11
        return (self.ranks[self.rank] + " of " + self.suits[self.suit])
12
 
13
    def cmp(self, other):
14
        # Check the suits
15
        if self.suit > other.suit:
16
            return 1
17
        if self.suit < other.suit:
18
            return -1
19
        # Suits are the same... check ranks
20
        if self.rank > other.rank:
21
            return 1
22
        if self.rank < other.rank:
23
            return -1
24
        # Ranks are the same... it's a tie
25
        return 0
26
 
27
    def __eq__(self, other):
28
        return self.cmp(other) == 0
29
 
30
    def __le__(self, other):
31
        return self.cmp(other) <= 0
32
 
33
    def __ge__(self, other):
34
        return self.cmp(other) >= 0
35
 
36
    def __gt__(self, other):
37
        return self.cmp(other) > 0
38
 
39
    def __lt__(self, other):
40
        return self.cmp(other) < 0
41
 
42
    def __ne__(self, other):
43
        return self.cmp(other) != 0
44
 
45
class Deck:
46
 
47
    def __init__(self):
48
        self.cards = []
49
        for suit in range(4):
50
            for rank in range(1, 14):
51
                self.cards.append(Card(suit, rank))
52
 
53
    def __str__(self):
54
        s = ""
55
        for card in self.cards:
56
            s += str(card) + '\n'
57
        return s
58
 
59
    def shuffle(self):
60
        import random
61
        random.shuffle(self.cards)
62
 
63
    def remove(self, card):
64
        if card in self.cards:
65
            self.cards.remove(card)
66
            return True
67
        else:
68
            return False
69
 
70
    def pop(self):
71
        return self.cards.pop()
72
 
73
    def is_empty(self):
74
        return self.cards == []
75
 
76
    def deal(self, hands, numCards=999):
77
        numHands = len(hands)
78
        for i in range(numCards):
79
            if self.is_empty():
80
                break                    # Break if out of cards
81
            card = self.pop()            # Take the top card
82
            hand = hands[i % numHands]   # Whose turn is next?
83
            hand.add(card)               # Add the card to the hand
84
 
85
class Hand(Deck):
86
 
87
    def __init__(self, name=""):
88
        self.cards = []
89
        self.name = name
90
 
91
    def add(self, card):
92
        self.cards.append(card)
93
 
94
hand1 = Hand("Hand 1")
95
hand2 = Hand("Hand 2")
96
myDeck = Deck()
97
myDeck.shuffle()
98
# deal 5 cards to each player
99
myDeck.deal([hand1,hand2],10)
100
print(hand1)
101
print(hand2)
102
# should be 42 cards left in the deck
103
print(len(myDeck.cards))
 

The second parameter, numCards, is optional; the default is a large number, which effectively means that all of the cards in the deck will get dealt.

The loop variable i goes from 0 to numCards-1. Each time through the loop, a card is removed from the deck using the list method pop, which removes and returns the last item in the list.

The modulus operator (%) allows us to deal cards in a round robin (one card at a time to each hand). When i is equal to the number of hands in the list, the expression i % numHands wraps around to the beginning of the list (index 0).

15.4. Printing a Hand

To print the contents of a hand, we can take advantage of the __str__ method inherited from Deck. Although it is convenient to inherit the existing methods, there is additional information in a Hand object we might want to include when we print one. To do that, we can provide a __str__ method in the Hand class that overrides the one in the Deck class:

1
class Card:
2
    suits = ["Clubs", "Diamonds", "Hearts", "Spades"]
3
    ranks = ["narf", "Ace", "2", "3", "4", "5", "6", "7",
4
             "8", "9", "10", "Jack", "Queen", "King"]
5
 
6
    def __init__(self, suit=0, rank=0):
7
        self.suit = suit
8
        self.rank = rank
9
 
10
    def __str__(self):
11
        return (self.ranks[self.rank] + " of " + self.suits[self.suit])
12
 
13
    def cmp(self, other):
14
        # Check the suits
15
        if self.suit > other.suit:
16
            return 1
17
        if self.suit < other.suit:
18
            return -1
19
        # Suits are the same... check ranks
20
        if self.rank > other.rank:
21
            return 1
22
        if self.rank < other.rank:
23
            return -1
24
        # Ranks are the same... it's a tie
25
        return 0
26
 
27
    def __eq__(self, other):
28
        return self.cmp(other) == 0
29
 
30
    def __le__(self, other):
31
        return self.cmp(other) <= 0
32
 
33
    def __ge__(self, other):
34
        return self.cmp(other) >= 0
35
 
36
    def __gt__(self, other):
37
        return self.cmp(other) > 0
38
 
39
    def __lt__(self, other):
40
        return self.cmp(other) < 0
41
 
42
    def __ne__(self, other):
43
        return self.cmp(other) != 0
44
 
45
class Deck:
46
 
47
    def __init__(self):
48
        self.cards = []
49
        for suit in range(4):
50
            for rank in range(1, 14):
51
                self.cards.append(Card(suit, rank))
52
 
53
    def __str__(self):
54
        s = ""
55
        for card in self.cards:
56
            s += str(card) + '\n'
57
        return s
58
 
59
    def shuffle(self):
60
        import random
61
        random.shuffle(self.cards)
62
 
63
    def remove(self, card):
64
        if card in self.cards:
65
            self.cards.remove(card)
66
            return True
67
        else:
68
            return False
69
 
70
    def pop(self):
71
        return self.cards.pop()
72
 
73
    def is_empty(self):
74
        return self.cards == []
75
 
76
    def deal(self, hands, numCards=999):
77
        numHands = len(hands)
78
        for i in range(numCards):
79
            if self.is_empty():
80
                break                    # Break if out of cards
81
            card = self.pop()            # Take the top card
82
            hand = hands[i % numHands]  # Whose turn is next?
83
            hand.add(card)               # Add the card to the hand
84
 
85
class Hand(Deck):
86
 
87
    def __init__(self, name=""):
88
        self.cards = []
89
        self.name = name
90
 
91
    def add(self, card):
92
        self.cards.append(card)
93
 
94
    def __str__(self):
95
        s = "Hand " + self.name
96
        if self.is_empty():
97
            s += " is empty\n"
98
        else:
99
            s += " contains\n"
100
        return s + Deck.__str__(self)
101
 
102
hand1 = Hand("Hand 1")
103
hand2 = Hand("Hand 2")
104
myDeck = Deck()
105
myDeck.shuffle()
106
# deal 5 cards to each player
107
myDeck.deal([hand1,hand2],10)
108
print(hand1)
109
print(hand2)
 

Initially, s is a string that identifies the hand. If the hand is empty, the program appends the words is empty and returns s.

Otherwise, the program appends the word contains and the string representation of the Deck, computed by invoking the __str__ method in the Deck class on self.

It may seem odd to send self, which refers to the current Hand, to a Deck method, until you remember that a Hand is a kind of Deck. Hand objects can do everything Deck objects can, so it is legal to send a Hand to a Deck method.

In general, it is always legal to use an instance of a subclass in place of an instance of a parent class.

15.5. The CardGame class

1
class CardGame:
2
 
3
    def __init__(self):
4
        self.deck = Deck()
5
        self.deck.shuffle()
 

This is the first case we have seen where the initialization method performs a significant computation, beyond initializing attributes.

To implement specific games, we can inherit from CardGame and add features for the new game. As an example, we’ll write a simulation of Old Maid.

The object of Old Maid is to get rid of cards in your hand. You do this by matching cards by rank and color. For example, the 4 of Clubs matches the 4 of Spades since both suits are black. The Jack of Hearts matches the Jack of Diamonds since both are red.

To begin the game, the Queen of Clubs is removed from the deck so that the Queen of Spades has no match. The fifty-one remaining cards are dealt to the players in a round robin. After the deal, all players match and discard as many cards as possible.

When no more matches can be made, play begins. In turn, each player picks a card (without looking) from the closest neighbor to the left who still has cards. If the chosen card matches a card in the player’s hand, the pair is removed. Otherwise, the card is added to the player’s hand. Eventually all possible matches are made, leaving only the Queen of Spades in the loser’s hand.

In our computer simulation of the game, the computer plays all hands. Unfortunately, some nuances of the real game are lost. In a real game, the player with the Old Maid goes to some effort to get their neighbor to pick that card, by displaying it a little more prominently, or perhaps failing to display it more prominently, or even failing to fail to display that card more prominently. The computer simply picks a neighbor’s card at random.

15.6. OldMaidHand class

A hand for playing Old Maid requires some abilities beyond the general abilities of a Hand. We will define a new class, OldMaidHand, that inherits from Hand and provides an additional method called remove_matches:

1
class OldMaidHand(Hand):
2
 
3
    def remove_matches(self):
4
        count = 0
5
        originalCards = self.cards[:]
6
        for card in originalCards:
7
            match = Card(3 - card.suit, card.rank)
8
            if match in self.cards:
9
                self.cards.remove(card)
10
                self.cards.remove(match)
11
                print("Hand "+self.name+":",end=' ')
12
                print(str(card)+" matches "+str(match))
13
                count += 1
14
        return count
 

We start by making a copy of the list of cards so that we can traverse the copy while removing cards from the original. Since self.cards is modified in the loop, we don’t want to use it to control the traversal. Python can get quite confused if it is traversing a list that is changing!

For each card in the hand, we figure out what the matching card is and go looking for it. The match card has the same rank and the other suit of the same color. The expression 3 - card.suit turns a Club (suit 0) into a Spade (suit 3) and a Diamond (suit 1) into a Heart (suit 2). You should satisfy yourself that the opposite operations also work. If the match card is also in the hand, both cards are removed.

The following example adds our new classes to our existing code, and demonstrates how to use remove_matches:

1
class Card:
2
    suits = ["Clubs", "Diamonds", "Hearts", "Spades"]
3
    ranks = ["narf", "Ace", "2", "3", "4", "5", "6", "7",
4
             "8", "9", "10", "Jack", "Queen", "King"]
5
 
6
    def __init__(self, suit=0, rank=0):
7
        self.suit = suit
8
        self.rank = rank
9
 
10
    def __str__(self):
11
        return (self.ranks[self.rank] + " of " + self.suits[self.suit])
12
 
13
    def cmp(self, other):
14
        # Check the suits
15
        if self.suit > other.suit:
16
            return 1
17
        if self.suit < other.suit:
18
            return -1
19
        # Suits are the same... check ranks
20
        if self.rank > other.rank:
21
            return 1
22
        if self.rank < other.rank:
23
            return -1
24
        # Ranks are the same... it's a tie
25
        return 0
26
 
27
    def __eq__(self, other):
28
        return self.cmp(other) == 0
29
 
30
    def __le__(self, other):
31
        return self.cmp(other) <= 0
32
 
33
    def __ge__(self, other):
34
        return self.cmp(other) >= 0
35
 
36
    def __gt__(self, other):
37
        return self.cmp(other) > 0
38
 
39
    def __lt__(self, other):
40
        return self.cmp(other) < 0
41
 
42
    def __ne__(self, other):
43
        return self.cmp(other) != 0
44
 
45
class Deck:
46
 
47
    def __init__(self):
48
        self.cards = []
49
        for suit in range(4):
50
            for rank in range(1, 14):
51
                self.cards.append(Card(suit, rank))
52
 
53
    def __str__(self):
54
        s = ""
55
        for card in self.cards:
56
            s += str(card) + '\n'
57
        return s
58
 
59
    def shuffle(self):
60
        import random
61
        random.shuffle(self.cards)
62
 
63
    def remove(self, card):
64
        if card in self.cards:
65
            self.cards.remove(card)
66
            return True
67
        else:
68
            return False
69
 
70
    def pop(self):
71
        return self.cards.pop()
72
 
73
    def is_empty(self):
74
        return self.cards == []
75
 
76
    def deal(self, hands, numCards=999):
77
        numHands = len(hands)
78
        for i in range(numCards):
79
            if self.is_empty():
80
                break                    # Break if out of cards
81
            card = self.pop()            # Take the top card
82
            hand = hands[i % numHands]  # Whose turn is next?
83
            hand.add(card)               # Add the card to the hand
84
 
85
class Hand(Deck):
86
 
87
    def __init__(self, name=""):
88
        self.cards = []
89
        self.name = name
90
 
91
    def add(self, card):
92
        self.cards.append(card)
93
 
94
    def __str__(self):
95
        s = "Hand " + self.name
96
        if self.is_empty():
97
            s += " is empty\n"
98
        else:
99
            s += " contains\n"
100
        return s + Deck.__str__(self)
101
 
102
class CardGame:
103
 
104
    def __init__(self):
105
        self.deck = Deck()
106
        self.deck.shuffle()
107
 
108
class OldMaidHand(Hand):
109
 
110
    def remove_matches(self):
111
        count = 0
112
        originalCards = self.cards[:]
113
        for card in originalCards:
114
            match = Card(3 - card.suit, card.rank)
115
            if match in self.cards:
116
                self.cards.remove(card)
117
                self.cards.remove(match)
118
                print("Hand "+self.name+":",end=' ')
119
                print(str(card)+" matches "+str(match))
120
                count += 1
121
        return count
122
 
123
game = CardGame()             # start a new game
124
hand = OldMaidHand("Frank")   # initialize a new Old Maid hand called "Frank"
125
game.deck.deal([hand], 13)    # deal 13 cards to Frank
126
print(hand)
127
hand.remove_matches()         # remove the matches
128
print(hand)
 

Notice that there is no __init__ method for the OldMaidHand class. We inherit it from Hand.

15.7. OldMaidGame class

Now we can turn our attention to the game itself. OldMaidGame is a subclass of CardGame with a new method called play that takes a list of players as a parameter.

Since __init__ is inherited from CardGame, a new OldMaidGame object contains a new shuffled deck:

1
class OldMaidGame(CardGame):
2
 
3
    def play(self, names):
4
        # Remove Queen of Clubs
5
        self.deck.remove(Card(0,12))
6
 
7
        # Make a hand for each player
8
        self.hands = []
9
        for name in names:
10
            self.hands.append(OldMaidHand(name))
11
 
12
        # Deal the cards
13
        self.deck.deal(self.hands)
14
        print("---------- Cards have been dealt")
15
        self.print_hands()
16
 
17
        # Remove initial matches
18
        matches = self.remove_all_matches()
19
        print("---------- Matches discarded, play begins")
20
        self.print_hands()
21
 
22
        # Play until all 50 cards are matched
23
        turn = 0
24
        numHands = len(self.hands)
25
        while matches < 25:
26
            matches += self.play_one_turn(turn)
27
            turn = (turn + 1) % numHands
28
 
29
        print("---------- Game is Over")
30
        self.print_hands()
 

Some of the steps of the game have been separated into methods that we still have to write.

print_hands just traverses the list of hands and prints each one:

1
    def print_hands(self):
2
        for hand in self.hands:
3
            print(hand)
 

remove_all_matches traverses the list of hands and invokes remove_matches on each:

1
    def remove_all_matches(self):
2
        count = 0
3
        for hand in self.hands:
4
            count += hand.remove_matches()
5
        return count
 

count is an accumulator that adds up the number of matches in each hand. When we’ve gone through every hand, the total is returned (count).

When the total number of matches reaches twenty-five, fifty cards have been removed from the hands, which means that only one card is left and the game is over.

The variable turn keeps track of which player’s turn it is. It starts at 0 and increases by one each time; when it reaches numHands, the modulus operator wraps it back around to 0.

The method play_one_turn takes a parameter that indicates whose turn it is. The return value is the number of matches made during this turn:

1
    def play_one_turn(self, i):
2
        if self.hands[i].is_empty():
3
            return 0
4
        neighbor = self.find_neighbor(i)
5
        pickedCard = self.hands[neighbor].pop()
6
        self.hands[i].add(pickedCard)
7
        print("Hand", self.hands[i].name, "picked", pickedCard)
8
        count = self.hands[i].remove_matches()
9
        self.hands[i].shuffle()
10
        return count
 

If a player’s hand is empty, that player is out of the game, so he or she does nothing and returns 0.

Otherwise, a turn consists of finding the first player on the left that has cards, taking one card from the neighbor, and checking for matches. Before returning, the cards in the hand are shuffled so that the next player’s choice is random.

The method find_neighbor starts with the player to the immediate left and continues around the circle until it finds a player that still has cards:

1
    def find_neighbor(self, i):
2
        numHands = len(self.hands)
3
        for countToTheLeft in range(1,numHands):
4
            neighbor = (i + countToTheLeft) % numHands
5
            if not self.hands[neighbor].is_empty():
6
                return neighbor
 

If find_neighbor ever went all the way around the circle without finding cards, it would return None and cause an error elsewhere in the program. Fortunately, we can prove that that will never happen (as long as the end of the game is detected correctly).

Let’s add all these new methods to our code, and then you can play the game!

1
class Card:
2
    suits = ["Clubs", "Diamonds", "Hearts", "Spades"]
3
    ranks = ["narf", "Ace", "2", "3", "4", "5", "6", "7",
4
             "8", "9", "10", "Jack", "Queen", "King"]
5
 
6
    def __init__(self, suit=0, rank=0):
7
        self.suit = suit
8
        self.rank = rank
9
 
10
    def __str__(self):
11
        return (self.ranks[self.rank] + " of " + self.suits[self.suit])
12
 
13
    def cmp(self, other):
14
        # Check the suits
15
        if self.suit > other.suit:
16
            return 1
17
        if self.suit < other.suit:
18
            return -1
19
        # Suits are the same... check ranks
20
        if self.rank > other.rank:
21
            return 1
22
        if self.rank < other.rank:
23
            return -1
24
        # Ranks are the same... it's a tie
25
        return 0
26
 
27
    def __eq__(self, other):
28
        return self.cmp(other) == 0
29
 
30
    def __le__(self, other):
31
        return self.cmp(other) <= 0
32
 
33
    def __ge__(self, other):
34
        return self.cmp(other) >= 0
35
 
36
    def __gt__(self, other):
37
        return self.cmp(other) > 0
38
 
39
    def __lt__(self, other):
40
        return self.cmp(other) < 0
41
 
42
    def __ne__(self, other):
43
        return self.cmp(other) != 0
44
 
45
class Deck:
46
 
47
    def __init__(self):
48
        self.cards = []
49
        for suit in range(4):
50
            for rank in range(1, 14):
51
                self.cards.append(Card(suit, rank))
52
 
53
    def __str__(self):
54
        s = ""
55
        for card in self.cards:
56
            s += str(card) + '\n'
57
        return s
58
 
59
    def shuffle(self):
60
        import random
61
        random.shuffle(self.cards)
62
 
63
    def remove(self, card):
64
        if card in self.cards:
65
            self.cards.remove(card)
66
            return True
67
        else:
68
            return False
69
 
70
    def pop(self):
71
        return self.cards.pop()
72
 
73
    def is_empty(self):
74
        return self.cards == []
75
 
76
    def deal(self, hands, numCards=999):
77
        numHands = len(hands)
78
        for i in range(numCards):
79
            if self.is_empty():
80
                break                    # Break if out of cards
81
            card = self.pop()            # Take the top card
82
            hand = hands[i % numHands]  # Whose turn is next?
83
            hand.add(card)               # Add the card to the hand
84
 
85
class Hand(Deck):
86
 
87
    def __init__(self, name=""):
88
        self.cards = []
89
        self.name = name
90
 
91
    def add(self, card):
92
        self.cards.append(card)
93
 
94
    def __str__(self):
95
        s = "Hand " + self.name
96
        if self.is_empty():
97
            s += " is empty\n"
98
        else:
99
            s += " contains\n"
100
        return s + Deck.__str__(self)
101
 
102
class CardGame:
103
 
104
    def __init__(self):
105
        self.deck = Deck()
106
        self.deck.shuffle()
107
 
108
class OldMaidHand(Hand):
109
 
110
    def remove_matches(self):
111
        count = 0
112
        originalCards = self.cards[:]
113
        for card in originalCards:
114
            match = Card(3 - card.suit, card.rank)
115
            if match in self.cards:
116
                self.cards.remove(card)
117
                self.cards.remove(match)
118
                print("Hand "+self.name+":",end=' ')
119
                print(str(card)+" matches "+str(match))
120
                count += 1
121
        return count
122
 
123
class OldMaidGame(CardGame):
124
 
125
    def play(self, names):
126
        # Remove Queen of Clubs
127
        self.deck.remove(Card(0,12))
128
 
129
        # Make a hand for each player
130
        self.hands = []
131
        for name in names:
132
            self.hands.append(OldMaidHand(name))
133
 
134
        # Deal the cards
135
        self.deck.deal(self.hands)
136
        print("---------- Cards have been dealt")
137
        self.print_hands()
138
 
139
        # Remove initial matches
140
        matches = self.remove_all_matches()
141
        print("---------- Matches discarded, play begins")
142
        self.print_hands()
143
 
144
        # Play until all 50 cards are matched
145
        turn = 0
146
        numHands = len(self.hands)
147
        while matches < 25:
148
            matches += self.play_one_turn(turn)
149
            turn = (turn + 1) % numHands
150
 
151
        print("---------- Game is Over")
152
        self.print_hands()
153
 
154
    def print_hands(self):
155
        for hand in self.hands:
156
            print(hand)
157
 
158
    def remove_all_matches(self):
159
        count = 0
160
        for hand in self.hands:
161
            count += hand.remove_matches()
162
        return count
163
 
164
    def play_one_turn(self, i):
165
        if self.hands[i].is_empty():
166
            return 0
167
        neighbor = self.find_neighbor(i)
168
        pickedCard = self.hands[neighbor].pop()
169
        self.hands[i].add(pickedCard)
170
        print("Hand", self.hands[i].name, "picked", pickedCard)
171
        count = self.hands[i].remove_matches()
172
        self.hands[i].shuffle()
173
        return count
174
 
175
    def find_neighbor(self, i):
176
        numHands = len(self.hands)
177
        for countToTheLeft in range(1,numHands):
178
            neighbor = (i + countToTheLeft) % numHands
179
            if not self.hands[neighbor].is_empty():
180
                return neighbor
181
 
182
game = OldMaidGame()
183
game.play(["Allen","Betsy","Carl","Diane"])
 

15.8. Glossary

inheritance
The ability to define a new class that is a modified version of a previously defined class.
parent class
The class from which a child class inherits.
child class
A new class created by inheriting from an existing class; also called a subclass.