Subclassing python objects

Reverend Jim
Subclassing python objects

Sometimes you want to add just a little more functionality to an existing python object. You can do that by subclassing it and adding on the missing parts. This example is based on a really mindless game of solitaire I used to play as a kid. In this game you start with a shuffled deck. You deal the cards face up one by one. Play doesn't actually start until you have four cards laid out (place them face up, left to right, or fan them out in your hand). Every time you add a card you examine the four most recent cards (rightmost).

If the last card and the fourth last card are the same rank then you remove the last four cards.

If the last card and the fourth last card are the same suit then you remove the two cards between them.

Play continues until you have dealt all the cards. If you have no cards left in your hand at that point then you win.

I knew it was difficult to win a hand (based on never having done so). I wanted to know if it was even possible. Rather than playing and hoping for a win I decided to script it. My first attempt a couple of years back was written in vbScript. This version is written in python. For the record, the vbScript version is 143 lines. The python version is 54 (comments and blank lines not counted).

The basic unit of the game is a card, which consists of a rank (A23..TJQK) and a suit. Both a hand and a deck consist of a list of cards. For the purpose of this game, the only functionality I want to add is the ability to produce a compact string for printing the results.

There are a number of functions that operate the same no matter what objects they are used on. For example, you can get the length of something with the len(something) function, or convert something to a string with the str(something) function. Similarly, you can compare things with logical operators like ==, <=, etc. These are actually implemented on an object by object basis by using the so-called magic methods. I can show you how this works with the card object which (in part) looks like

class Card:

    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit

By redefining the magic method __eq__ as follows:

def __eq__(self, other):
    return (self.rank == other.rank and self.suit == other.suit)

I can now compare two card objects for equality by

if carda == cardb:

and python will use my overridden __eq__ method to do the comparison. Likewise, I can redefine the magic method __str__ as

def __str__(self):
    return str(self.rank) + self.suit

and when I want to convert card to a string I can just do

str(card)

I have similarly redefined the __str__ method in both the Hand and Deck classes. Purists would say that Hand and Deck are so similar that they should not be separate classes but I thought I would just keep it simple here.

"""                                                                             
    Name:                                                                       

      Solitaire.py                                                             

    Description:                                                                

      This program simulates multiple runs of a pretty mindless game of         
      solitaire that relies only on the luck of the cards. There is no skill    
      involved.                                                                 

      In this game, cards are pulled from the deck one at a time and added to   
      your hand. If you have two cards exactly four apart that have the same    
      suit, remove the two cards between these cards. If you have two cards     
      exactly four cards apart of the same rank remove these two cards AND the  
      two cards between them. The idea is to go through the entire deck and end 
      up with as few cards as possible.                                         

      I wanted to know if it was actually possible to end up with zero cards    
      but I didn't feel like playing for long enough to win so I wrote this     
      instead. It displays the game number, the starting deck, then the hand    
      after every change (card added/cards deleted). It stops when a win is     
      detected (no cards remaining).                                            

    Audit:                                                                      

         2020-08-27  rj  original code                                          

"""                                                                            

import random

class Card:

    def __init__(self, rank, suit):
        self.rank = rank
        self.suit = suit

    def __eq__(self, other):
        return (self.rank == other.rank and self.suit == other.suit)

    def __str__(self):
        return str(self.rank) + self.suit

class Hand(list):

    def __init__(self):
        super().__init__()

    def __str__(self):

        s = ""

        for card in self:
            s += " " + str(card)

        return s.strip()

class Deck(list):

    def __init__(self):
        super().__init__()

        suits = (chr(9824), chr(9829), chr(9830), chr(9827))

        for i in range(52):
            card = Card("A23456789TJQK"[i % 13], suits[i//13])
            self.append(card)

    def __str__(self):

        s = ""

        for card in self:
            s += " " + str(card)

        return s.strip()

games = 0
random.seed()

while True:

    games += 1

    deck = Deck()
    hand = Hand()

    random.shuffle(deck)

    print("\ngame #%d\n" % games)

    #loop until the deck is empty

    while len(deck):

        #draw another card and display the current hand

        hand.append(deck.pop())
        print(str(hand))

        #remove cards as long as we are able

        while True:

            count = len(hand)

            #if rank of last) card = rank of 4th last card then remove last 4

            if len(hand) > 3 and hand[-1].rank == hand[-4].rank:
                for i in range(4): del hand[-1]
                print(str(hand))

            #if suit of last card = suit of 4th last card then remove cards between

            if len(hand) > 3 and hand[-1].suit == hand[-4].suit:
                 for i in range(2): del hand[-2]
                 print(str(hand))

            #quit if no cards were removed
            if count == len(hand): break    

    if len(hand):
        print("\nLOST - %d cards left" % len(hand))
    else:
        print("WON in %d games" % games)

    #if no cards left then we won
    if not len(hand): break
29 Views
About the Author

I completed my Computer Science degree at the University of Manitoba in 1976. I did two and a half years of programming in medical research followed by twenty-nine years at Manitoba Hydro (electric utility). Most of that was spent on doing development and maintenance on an AGC/SCADA (real-time programming/process control) system. The last ten years of that was spent doing application and infrastructure support and development. I have programmed in FORTRAN (mostly), APL, PL/1, COBOL, Lisp, SNOBOL, ALGOL, Assembler (several flavours), C, C++, Paradox, VB, vbScript and more recently, Python. I am married with two grown children of whom I am very proud, and a most beautiful wife. I am currently retired (and loving it).