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