Follow Techotopia on Twitter

On-line Guides
All Guides
eBook Store
iOS / Android
Linux for Beginners
Office Productivity
Linux Installation
Linux Security
Linux Utilities
Linux Virtualization
Linux Kernel
System/Network Admin
Programming
Scripting Languages
Development Tools
Web Development
GUI Toolkits/Desktop
Databases
Mail Systems
openSolaris
Eclipse Documentation
Techotopia.com
Virtuatopia.com

How To Guides
Virtualization
General System Admin
Linux Security
Linux Filesystems
Web Servers
Graphics & Desktop
PC Hardware
Windows
Problem Solutions

  




 

 

Class Definition Exercises

These exercises are considerably more sophisticated then the exercises in previous parts. Each of these sections describes a small project that requires you to create a number of distinct classes which must collaborate to produce a useful result.

Stock Valuation

A Block of stock has a number of attributes, including a purchase price, purchase date, and number of shares. Commonly, methods are needed to compute the total spent to buy the stock, and the current value of the stock. A Position is the current ownership of a company reflected by all of the blocks of stock. A Portfolio is a collection of Positions; it has methods to compute the total value of all Blocks of stock.

When we purchase stocks a little at a time, each Block has a different price. We want to compute the total value of the entire set of Blocks, plus an average purchase price for the set of Blocks.

The StockBlock class. First, define a StockBlock class which has the purchase date, price per share and number of shares. Here are the method functions this class should have.

__init__

The __init__ method will populate the individual fields of date, price and number of shares. Don't include the company name or ticker symbol; this is information which is part of the Position, not the individual blocks.

__str__

The __str__ method must return a nicely formatted string that shows the date, price and shares.

getPurchValue

The getPurchValue method should compute the value as purchase price per share × shares.

getSaleValue ( salePrice )

The getSaleValue method requires a salePrice ; it computes the value as sale price per share × shares.

getROI ( salePrice )

The getROI method requires a salePrice ; it computes the return on investment as (sale value - purchase value) ÷ purchase value.

We can load a simple database with a piece of code the looks like the following. The first statement will create a sequence with four blocks of stock. We chose variable name that would remind us that the ticker symbols for all four is 'GM'. The second statement will create another sequence with four blocks.

blocksGM = [ 
StockBlock( purchDate='25-Jan-2001', purchPrice=44.89, shares=17 ),
StockBlock( purchDate='25-Apr-2001', purchPrice=46.12, shares=17 ),
StockBlock( purchDate='25-Jul-2001', purchPrice=52.79, shares=15 ),
StockBlock( purchDate='25-Oct-2001', purchPrice=37.73, shares=21 ),
]
blocksEK = [ 
StockBlock( purchDate='25-Jan-2001', purchPrice=35.86, shares=22 ),
StockBlock( purchDate='25-Apr-2001', purchPrice=37.66, shares=21 ),
StockBlock( purchDate='25-Jul-2001', purchPrice=38.57, shares=20 ),
StockBlock( purchDate='25-Oct-2001', purchPrice=27.61, shares=28 ),
]

The Position class. A separate class, Position, will have an the name, symbol and a sequence of StockBlocks for a given company. Here are some of the method functions this class should have.

__init__

The __init__ method should accept the company name, ticker symbol and a collection of StockBlock instances.

__str__

The __str__ method should return a string that contains the symbol, the total number of shares in all blocks and the total purchse price for all blocks.

getPurchValue

The getPurchValue method sums the purchase values for all of the StockBlocks in this Position. It delegates the hard part of the work to each StockBlock's getPurchValue method.

getSaleValue ( salePrice )

The getSaleValue method requires a salePrice ; it sums the sale values for all of the StockBlocks in this Position. It delegates the hard part of the work to each StockBlock's getSaleValue method.

getROI

The getROI method requires a salePrice ; it computes the return on investment as (sale value - purchase value) ÷ purchase value. This is an ROI based on an overall yield.

We can create our Position objects with the following kind of initializer. This creates a sequence of three individual Position objects; one has a sequence of GM blocks, one has a sequence of EK blocks and the third has a single CAT block.

portfolio= [
    Position( "General Motors", "GM", blocksGM ),
    Position( "Eastman Kodak", "EK", blocksEK )
    Position( "Caterpillar", "CAT", 
        [ StockBlock( purchDate='25-Oct-2001', 
            purchPrice=42.84, shares=18 ) ] )
]

An Analysis Program. You can now write a main program that writes some simple reports on each Position object in the portfolio. One report should display the individual blocks purchased, and the purchase value of the block. This requires iterating through the Positions in the portfolio, and then delegating the detailed reporting to the individual StockBlocks within each Position.

Another report should summarize each position with the symbol, the total number of shares and the total value of the stock purchased. The overall average price paid is the total value divided by the total number of shares.

In addition to the collection of StockBlocks that make up a Position, one additional piece of information that is useful is the current trading price for the Position. First, add a currentPrice attribute, and a method to set that attribute. Then, add a getCurrentValue method which computes a sum of the getSaleValue method of each StockBlock. using the trading price of the Position.

Annualized Return on Investment. In order to compare portfolios, we might want to compute an annualized ROI. This is ROI as if the stock were held for eactly one year. In this case, since each block has different ownership period, the annualized ROI of each block has to be computed. Then we return an average of each annual ROI weighted by the sale value.

The annualization requires computing the duration of stock ownership. This requires use of the time module. We'll cover that in depth in Chapter 32, Dates and Times: the time and datetime Modules . The essential feature, however, is to parse the date string to create a time object and then get the number of days between two time objects. Here's a code snippet that does most of what we want.

>>> 

import time

>>> 

dt1= "25-JAN-2001"

>>> 

timeObj1= time.strptime( dt1, "%d-%b-%Y" )

>>> 

dayNumb1= int(time.mktime( timeObj1 ))/24/60/60

>>> 

dt2= "25-JUN-2001"

>>> 

timeObj2= time.strptime( dt2, "%d-%b-%Y" )

>>> 

dayNumb2= int(time.mktime( timeObj2 ))/24/60/60

>>> 

dayNumb2 - dayNumb1

151

In this example, timeObj1 and timeObj2 are time structures with details parsed from the date string by time.strptime. The dayNumb1 and dayNumb2 are a day number that corresponds to this time. Time is measured in seconds after an epoch; typically January 1, 1970. The exact value doesn't matter, what matters is that the epoch is applied consistently by mktime. We divide this by 24 hours per day, 60 minutes per hour and 60 seconds per minute to get days after the epoch instead of seconds. Given two day numbers, the difference is the number of days between the two dates. In this case, there are 151 days between the two dates.

All of this processing must be encapsulated into a method that computes the ownership duration.

def ownedFor(self, saleDate ):

This method computes the days the stock was owned.

def annualizedROI(self, salePrice , saleDate ):

We would need to add an annualizedROI method to the StockBlock that divides the gross ROI by the duration in years to return the annualized ROI. Similarly, we would add a method to the Position to use the annualizedROI to compute the a weighted average which is the annualized ROI for the entire position.

Dive Logging and Surface Air Consumption Rate

The Surface Air Consumption Rate is used by SCUBA divers to predict air used at a particular depth. If we have a sequence of Dive objects with the details of each dive, we can do some simple calculations to get averages and ranges for our air consumption rate.

For each dive, we convert our air consumption at that dive's depth to a normalized air consumption at the surface. Given depth (in feet), d , starting tank pressure (psi), s , final tank pressure (psi), f , and time (in minutes) of t , the SACR, c , is given by the following formula.

Typically, you will average the SACR over a number of similar dives.

The Dive Class. You will want to create a Dive class that contains attributes which include start pressure, finish pressure, time and depth. Typical values are a starting pressure of 3000, ending pressure of 700, depth of 30 to 80 feet and times of 30 minutes (at 80 feet) to 60 minutes (at 30 feet). SACR's are typically between 10 and 20. Your Dive class should have a function named getSACR which returns the SACR for that dive.

To make life a little simpler putting the data in, we'll treat time as string of “HH:MM”, and use string functions to pick this apart into hours and minutes. We can save this as tuple of two intgers: hours and minutes. To compute the duration of a dive, we need to normalize our times to minutes past midnight, by doing hh*60+mm. Once we have our times in minutes past midnight, we can easily subtract to get the number of minutes of duration for the dive. You'll want to create a function getDuration to do just this computation for each dive.

__init__

The __init__ method will initialize a Dive with the start and finish pressure in PSI, the in and out time as a string, and the depth as an integer. This method should parse both the in string and out string and normalize each to be minutes after midnight so that it can compute the duration of the dive. Note that a practical dive log would have additional information like the date, the location, the air and water temperature, sea state, equipment used and other comments on the dive.

__str__

The __str__ method should return a nice string representation of the dive information.

getSACR

The getSACR method can then compute the SACR value from the starting pressure, final pressure, time and depth information.

The Dive Log. We'll want to initialize our dive log as follows:

log = [
    Dive( start=3100, finish=1300, in="11:52", out="12:45", depth=35 ),
    Dive( start=2700, finish=1000, in="11:16", out="12:06", depth=40 ),
    Dive( start=2800, finish=1200, in="11:26", out="12:06", depth=60 ),
    Dive( start=2800, finish=1150, in="11:54", out="12:16", depth=95 ),
]

Rather than use a simple sequence of Dives, you can create a DiveLog class which has a sequence of Dives plus a getAvgSACR method. Your DiveLog method can be initiatlized with a sequence of dives, and can have an append method to put another dive into the sequence.

Exercising the Dive and DiveLog Classes. Here's how the final application could look. Note that we're using an arbitrary number of argument values to the __init__ function, therefore, it has to be declared as def __init__( self, *listOfDives )

log= DiveLog(
    Dive( start=3100, finish=1300, in="11:52", out="12:45", depth=35 ),
    Dive( start=2700, finish=1000, in="11:16", out="12:06", depth=40 ),
    Dive( start=2800, finish=1200, in="11:26", out="12:06", depth=60 ),
    Dive( start=2800, finish=1150, in="11:54", out="12:16", depth=95 ), 
)
print log.getAvgSACR()
for d in log.dives:
    print d

Multi-Dice

If we want to simulate multi-dice games like Yacht, Kismet, Yatzee, Zilch, Zork, Greed or Ten Thousand, we'll need a collection that holds more than two dice. The most common configuration is a five-dice collection. In order to be flexible, we'll need to define a Dice object which will use a tuple, list or Set of individual Die instances. Since the number of dice in a game rarely varies, we can also use a FrozenSet.

Once you have a Dice class which can hold a collection of dice, you can gather some statistics on various multi-dice games. These games fall into two types. In both cases, the player's turn starts with rolling all the dice, the player can then elect to re-roll or preserve selected dice.

  • Scorecard Games. In Yacht, Kismet and Yatzee, five dice are used. The first step in a player's turn is a roll of all five dice. This can be followed by up to two additional steps in which the player decides which dice to preserve and which dice to roll. The player is trying to make a scoring hand. A typical scorecard for these games lists a dozen or more "hands" with associated point values. The player attempts to make one of each of the various kinds of hands listed on the scorecard. Each turn must fill in one line of the scorecard; if the dice match a hand which has not been scored, the player enters a score. If a turn does not result in a hand that matches an unscored hand, then a score of zero is entered.

  • Point Games. In Zilch, Zork, Green or Ten Thousand, five dice are typical, but there are some variations. The player in this game has no limit on the number of steps in their turn. The first step is to roll all the dice and determine a score. Their turn ends when they perceive the risk of another step to be too high, or they've made a roll which gives them a score of zero (or zilch) for the turn. Typically, if the newly rolled dice are non-scoring, their turn is over with a score of zero. At each step, the player is looking at newly rolled dice which improve their score. A straight scores 1000. Three-of-a-kind scores 100╳ the die's value (except three ones is 1000 points). After removing any three-of-a-kinds, each die showing 1 scores 100, each die showing 5 scores 50. Additionally, some folks will score 1000╳ the die's value for five-of-a-kind.

Our MultiDice class will be based on the example of Dice in this chapter. In addition to a collection of Die instances (a sequence, Set or FrozenSet), the class will have the following methods.

__init__

When initializing an instance of MultiDice, you'll create a collection of five individual Die instances. You can use a sequence of some kind, a Set or a FrozenSet.

roll

The roll method will roll all dice in the sequence or Set. Note that your choice of collection doesn't materially alter this method. That's a cool feature of Python.

getDice

This method returns the collection of dice so that a client class can examine them and potentialy re-roll some or all of the dice.

score

This method will score the hand, returning a list of two-tuples. Each two-tuple will have the name of the hand and the point value for the particular game. In some cases, there will be multiple ways to score a hand, and the list will reflect all possible scorings of the hand, in order from most valuable to least valuable. In other cases, the list will only have a single element.

It isn't practical to attempt to write a universal MultiDice class that covers all variations of dice games. Rather than write a gigantic does-everything class, the better policy is to create a family of classes that build on each other using inheritance. We'll look at this in the section called “Inheritance”. For this exercise, you'll have to pick one of the two families of games and compute the score for that particular game. Later, we'll see how to create an inheritance hierarchy that can cover the two-dice game of Craps as well as these multi-dice games.

For the scorecard games (Yacht, Kismet, Yatzee), we want to know if this set of dice matches any of the scorecard hands. In many cases, a set of dice can match a number of potential hands. A hand of all five dice showing the same value (e.g, a 6) is matches the sixes, three of a kind, four of a kind, five of a kind and wild-card rows on most game score-sheets. A sequence of five dice will match both a long straight and a short straight.

Common Scoring Methods. No matter which family of games you elect to pursue, you'll need some common method functions to help score a hand. The following methods will help to evaluate a set of dice to see which hand it might be.

  • matchDie. This function will take a Die and a Dice set. It uses matchValue to partition the dice based on the value of the given Die.

  • matchValue. This function is like matchDie, but it uses a numeric value instead of a Die. It partitions the dice into two sets: the dice in the Dice set which have a value that matches the given Die, and the remaining Die which do not match the value.

  • threeOfAKind , fourOfAKind , fiveOfAKind . These three functions will compute the matchDie for each Die in the Dice set. If any given Die has a matchDie with 3 (or 4 or 5) matching dice, the hand as a whole matches the template.

  • largeStraight . This function must establish that all five dice form a sequence of values from 1 to 5 or 2 to 6. There must be no gaps and no duplicated values.

  • smallStraight . This function must establish that four of the five dice form a sequence of values. There are a variety of ways of approaching this; it is actually a challenging algorithm. If we create a sequence of dice, and sort them into order, we're looking for an ascending sequence with one "irrelevant" die in it: this could be a gap before or after the sequence (1, 3, 4, 5, 6; 1, 2, 3, 4, 6 ) or a duplicated value (1, 2, 2, 3, 4, 5) within the sequence.

  • chance . The chance hand is simply the sum of the dice values. It is a number between 5 and 30.

This isn't necessarily the best way to do this. In many cases, a better way is to define a series of classes for the various kinds of hands, following the Strategy design pattern. The Dice would then have a collection of Strategy objects, each of which has a match method that compares the actual roll to a kind of hand. The Strategy objects would have a score method as well as a name method. This is something we'll look at in the section called “Strategy”.

Scoring Yacht, Kismet and Yatzee. For scoring these hands, you'll use the common scoring method functions. Your overall score method function will step through the candidate hands in a specific order. Generally, you'll want to check for fiveOfAKind first, since fourOfAKind and threeOfAKind will also be true for this hand. Similarly, you'll have to check for largeStraight before smallStraight.

Your score method will evaluate each of the scoring methods. If the method matches, your method will append a two-tuple with the name and points to the list of scores.

Scoring Zilch, Zork and 10,000. For scoring these dice throws, you'll need to expand on the basic threeOfAKind method. Your score method will make use of the two results sets created by the threeOfAKind method.

Note that the hand's description can be relatively complex. For example, you may have a hand with three 2's, a 1 and a 5. This is worth 350. The description has two parts: the three-of-a-kind and the extra 1's and 5's. Here are the steps for scoring this game.

  • Evaluate the largeStraight method. If the hand matches, then return a list with an appropriate 2-tuple.

  • If you're building a game variation where five of a kind is a scoring hand, then evaluate fiveOfAKind. If the hand matches, then return a list with an appropriate 2-tuple.

  • 3K. Evaluate the threeOfAKind method. This will create the first part of the hand's description.

    • If a Die created a matching set with exactly three dice, then the set of unmatched dice must be examined for additional 1's and 5's. The first part of the hand's description string is three-of-a-kind.

    • If a Die created a matching with four or five dice, then one or two dice must be popped from the matching set and added to the non-matching set. The set of unmatched dice must be examined for addtional 1's and 5's. The first part of the hand's description string is three-of-a-kind.

    • If there was no set of three matching dice, then all the dice are in the non-matching set, which is checked for 1's and 5's. The string which describes the hand has no first part, since there was no three-of-a-kind.

  • 1-5's. Any non-matching dice from the threeOfAKind test are then checked using matchValue to see if there are 1's or 5's. If there are any, this is the second part of the hand's description. If there are none, then there's no second part of the description.

  • The final step is to assemble the description. There are four cases: nothing, 3K with no 1-5's, 1-5's with no 3K, and 3K plus 1-5's. In the nothing case, this is a non-scoring hand. In the other cases, it is a scoring hand.

Exercising The Dice. Your main script should create a Dice set, execute an initial roll and score the result. It should then pick three dice to re-roll and score the result. Finally, it should pick one die, re-roll this die and score the result. This doesn't make sophisticated strategic decisions, but it does exercise your Dice and Die objects thoroughly.

When playing a scorecard game, the list of potential hands is examined to fill in another line on the scorecard. When playing a points game, each throw must result in a higher score than the previous throw or the turn is over.

Strategy. When playing these games, a person will be able to glance at the dice, form a pattern, and decide if the dice are "close" to one of the given hands. This is a challenging judgement, and requires some fairly sophisticated software to make a proper odd-based judgement of possible outcomes. Given that there are only 7,776 possible ways to roll 5 dice, it's a matter of exhaustively enumerating all of the potential outcomes of the various kinds of rerolls. This is an interesting, but quite advanced exercise.

Rational Numbers

A Rational number is a ratio of two integers. Examples include 1/2, 2/3, 22/7, 355/113, etc. We can do arithmetic operations on rational numbers. We can display them as proper fractions (3 1/7), improper fractions (22/7) or decimal expansions (3.1428571428571428).

The essence of this class is to perform arithmetic operations. We'll start by defining methods to add and multiply two rational values. Later, we'll delve into the additional methods you'd need to write to create a robust, complete implementation.

Your add and mul methods will perform their processing with two Rational values: self and other. In both cases, the variable other has to be another Rational number instance. You can check this by using the type function: if type(self) != type(other), you should raise a TypeException.

It's also important to note that all arithmetic operations will create a new Rational number computed from the inputs.

A Rational class has two attributes: the numerator and the denominator of the value. These are both integers. Here are the various methods you should created.

__init__

The __init__ constructor accepts the numerator and denominator values. It can have a default value for the denominator of 1. This gives us two constructors: Rational(2,3) and Rational(4). The first creates the fraction 2/3. The second creates the fraction 4/1.

This method should call the reduce method to assure that the fraction is properly reduced. For example, Rational(8,4) should automatically reduce to a numerator of 2 and a denominator of 1.

__str__

The __str__ method returns a nice string representation for the rational number, typically as an improper fraction. This gives you the most direct view of your Rational number.

You should provide a separate method to provide a proper fraction string with a whole number and a fraction. This other method would do additional processing to extract a whole name and remainder.

__float__

If you provide a method named __float__, this can return the floating-point value for the fraction. This method is called when a program does float( rationalValue ).

add ( self , other )

The add method creates and returns a new Rational number. This new fraction that has a numerator of (self.numerator × other.denominator + other.numerator × self.denominator), and a denominator of ( self.denominator × other.denominator ).

Equation 21.1. Adding Fractions

Example: 3/5 + 7/11 = (33 + 35)/55 = 71/55.

mul ( self , other )

The mul method creates and returns a new Rational number. This new fraction that has a numerator of (self.numerator × other.numerator), and a denominator of ( self.denominator × other.denominator ).

Equation 21.2. Multiplying Fractions

Example: 3/5 × 7/11 = 21/55.

reduce

In addition to adding and multiplying two Rational numbers, you'll also need to provide a reduce method which will reduce a fraction by removing the greatest common divisor from the numerator and the denominator. This should be called by __init__ to assure that all fractions are reduced.

To implement reduce, we find the greatest common divisor between the numerator and denominator and then divide both by this divisor. For example 8/4 has a GCD of 4, and reduces to 2/1. The Greatest Common Divisor (GCD) algorithm is given in Greatest Common Divisor and Greatest Common Divisor. If the GCD of self.numerator and self.denominator is 1, the reduced function can return self. Otherwise, reduced must create a new Rational with the reduced fraction.

Playing Cards and Decks

Standard playing cards have a rank (ace, two through ten, jack, queen and king) and suit (clubs, diamonds, hearts, spades). These form a nifty Card object with two simple attributes. We can add a few generally useful functions.

Here are the methods for your Card class.

__init__ ( self , rank )

The __init__ method sets the rank and suit of the card. The suits can be coded with a single character ("C", "D", "H", "S"), and the ranks can be coded with a number from 1 to 13. The number 1 is an ace. The numbers 11, 12, 13 are Jack, Queen and King, respectively. These are the ranks, not the point values.

__str__

The __str__ method can return the rank and suit in the form "2C" or "AS" or "JD". A rank of 1 would become "A", a rank of 11, 12 or 13 would become "J", "Q" or "K", respectively.

__cmp__ ( self , other )

If you define a __cmp__ method, this will used by the cmp function; the cmp function is used by the sort method of a list unless you provide an overriding function used for comparison. By providing a __cmp__ method in your class you can assure that cards are sorted by rank in preference to suit. You can also use <, >, >= and <= operations among cards.

Sometime as simple as cmp(self.rank,other.rank) or cmp(self.suit,other.suit) works surprisingly well.

Dealing and Decks.  Cards are dealt from a Deck; a collection of Cards that includes some methods for shuffling and dealing. Here are the methods that comprise a Deck.

__init__

The __init__ method creates all 52 cards. It can use two loops to iterate through the sequence of suits ("C", "D", "H", "S") and iterate through the ranks range(1,14). After creating each Card, it can append each Card to a sequence of Cards.

deal

The deal method should do two things: iterate through the sequence, exchanging each card with a randomly selected card. It turns out the random module has a shuffle function which does precisely this.

Dealing is best done with a generator method function. The deal method function should have a simple for-loop that yields each individual Card; this can be used by a client application to generate hands. The presence of the yield statement will make this method function usable by a for statement in a client application script.

Basic Testing. You should do some basic tests of your Card objects to be sure that they respond appropriately to comparison operations. For example,

>>> 
x1= Card(11,"C")

>>> 
x1

 JC
>>> 
x2= Card(12,"D")

>>> 
x1 < x2

True

You can write a simple test script which can the do the following to deal Cards from a Deck. In this example, the variable dealer will be the iterator object that the for statement uses internally.

d= Deck()
dealer= d.deal()
c1= dealer.next()
c2= dealer.next()

Hands. Many card games involve collecting a hand of cards. A Hand is a collection of Cards plus some addition methods to score the hand in way that's appropriate to the given game. We have a number of collection classes that we can use: list, tuple, dictionary and set.

Consider Blackjack. The Hand will have two Cards assigned initially; it will be scored. Then the player must choose among accepting another card (a hit), using this hand against the dealer (standing), doubling the bet and taking one more card, or splitting the hand into two hands. Ignoring the split option for now, it's clear that the collection of Cards has to grow and then get scored again. What are the pros and cons of list, tuple, set and dictionary?

Consider Poker. There are innumerable variations on poker; we'll look at simple five-card draw poker. Games like seven-card stud require you to score potential hands given only two cards, and as many as 21 alternative five-card hands made from seven cards. Texas Hold-Em has from three to five common cards plus two private cards, making the scoring rather complex. For five-card draw, the Hand will have five cards assigned initially, and it will be scored. Then some cards can be removed and replaced, and the hand scored again. Since a valid poker hand is an ascending sequence of cards, called a straight, it is handy to sort the collection of cards. What are the pros and cons of list, tuple, set and dictionary?

Blackjack Hands

Changes to the Card class. We'll extend our Card class to score hands in Blackjack, where the rank is used to determine the hand that is held. When used in Blackjack, a Card has a point value in addition to a rank and suit. Aces are either 1 or 11; two through ten are worth 2-10; the face cards are all worth 10 points. When an ace is counted as 1 point, the total is called the hard total. When an ace is counted as 11 points, the total is called a soft total.

You can add a point attribute to your card class. This can be set as part of __init__ processing. In that case, the following methods simple return the point value.

As an alternative, you can compute the point value each time it is requested. This has the obvious disadvantage of being slower. However, it is considerably simpler to add methods to a class without revising the existing __init__ method.

Here are the methods you'll need to add to your Card class in order to handle Blackjack hands.

getHardValue

The getHardValue method returns the rank, with the following exceptions: ranks of 11, 12 and 13 return a point value of 10.

getSoftValue

The getSoftValue method returns the rank, with the following exceptions: ranks of 11, 12 and 13 return a point value of 10; a rank of 1 returns a point value of 11.

As a teaser for the next chapter, we'll note that these methods should be part of a Blackjack-specific subclass of the generic Card class. For now, however, we'll just update the Card class definition.When we look at inheritance in the section called “Inheritance”, we'll see that a class hierarchy can be simpler than the if-statements in the getHardValue and getSoftValue methods.

Scoring Blackjack Hands. The objective of Blackjack is to accumulate a Hand with a total point value that is less than or equal to 21. Since an ace can count as 1 or 11, it's clear that only one of the aces in a hand can have a value of 11, and any other aces must have a value of 1.

Each Card produces a hard and soft point total. The Hand as a whole also has hard and soft point totals. Often, both hard and soft total are equal. When there is an ace, however, the hard and soft totals for the hand will be different. We have to look at two cases.

  • No Aces. The hard and soft total of the hand will be the same; it's the total of the hard value of each card. If the hard total is less than 21 the hand is in play. If it is equal to 21, it is a potential winner. If it is over 21, the hand has gone bust. Both totals will be computed as the hard value of all cards.

  • One or more Aces. The hard and soft total of the hand are different. The hard total for the hand is the sum of the hard point values of all cards. The soft total for the hand is the soft value of one ace plus the hard total of the rest of the cards. If the hard or soft total is 21, the hand is a potential winner. If the hard total is less than 21 the hand is in play. If the hard total is over 21, the hand has gone bust.

The Hand class has a collection of Cards, usually a sequence, but a Set will also work. Here are the methods of the Hand class.

__init__

The __init__ method should be given two instances of Card to represent the initial deal. It should create a sequence or Set with these two initial cards.

__str__

The __str__ method a string with all of the individual cards. A construct like the following works out well: ",".join( map(str,self.cards). This gets the string representation of each card in the self.cards collection, and then uses the string's join method to assemble the final display of cards.

hardTotal

The hardTotal method sums the hard value of each Card.

softTotal

The softTotal method is more complex. It needs to partition the cards into two sets. If there are any cards with a different hard and soft point value (this will be an ace), then one of these cards forms the softSet. The remaining cards form the hardSet. It's entirely possible that the softSet will be empty. It's also entirely possible that there are multiple cards which could be part of the softSet. The value of this function is the total of the hard values for all of the cards in the hardSet plus the soft value of the card in the softSet.

add

The add method will add another Card to the Hand.

Exercising Card, Deck and Hand. Once you have the Card, Deck and Hand classes, you can exercise these with a simple function to play one hand of blackjack. This program will create a Deck and a Hand; it will deal two Cards into the Hand. While the Hand's total is soft 16 or less, it will add Cards. Finally, it will print the resulting Hand.

There are two sets of rules for how to fill a Hand. The dealer is tightly constrained, but players are more free to make their own decisions. Note that the player's hands which go bust are settled immediately, irrespective of what happens to the dealer. On the other hand, the player's hands which total 21 aren't resolved until the dealer finishes taking cards.

The dealer must add cards to a hand with a soft 16 or less. If the dealer has a soft total between 17 and 21, they stop. If the dealer has a soft total which is over 21, but a hard total of 16 or less, they will take cards. If the dealer has a hard total of 17 or more, they will stop.

A player may add cards freely until their hard total is 21 or more. Typically, a player will stop at a soft 21; other than that, almost anything is possible.

Additional Plays. We've avoided discussing the options to split a hand or double the bet. These are more advanced topics that don't have much bearing on the basics of defining Card, Deck and Hand. Splitting simply creates additional Hands. Doubling down changes the bet and gets just one additional card.

Poker Hands

We'll extend the Card class we created in the section called “Playing Cards and Decks” to score hands in Poker, where both the rank and suit are used to determine the hand that is held.

Poker hands are ranked in the following order, from most desirable (and least likely) down to least desirable (and all too common).

  1. Straight Flush. Five cards of adjacent ranks, all of the same suit.

  2. Four of a Kind. Four cards of the same rank, plus another card.

  3. Full House. Three cards of the same rank, plus two cards of the same rank.

  4. Flush. Five cards of the same suit.

  5. Straight. Five cards of adjacent ranks. In this case, Ace can be above King or below 2.

  6. Three of a Kind. Three cards of the same rank, plus two cards of other ranks.

  7. Two Pair. Two cards of one rank, plus two cards of another rank, plus one card of a third rank.

  8. Pair. Two cards of one rank, plus three cards of other ranks.

  9. High Card. The highest ranking card in the hand.

Note that a straight flush is both a straight and a flush; four of a kind is also two pair as well as one pair; a full house is also two pair, as well as a one pair. It is important, then, to evaluate poker hands in decreasing order of importance in order to find the best hand possible.

In order to distinguish between two straights or two full-houses, it is important to also record the highest scoring card. A straight with a high card of a Queen, beats a straight with a high card of a 10. Similarly, a full house or two pair is described as “queens over threes”, meaning there are three queens and two threes comprising the hand. We'll need a numeric ranking that includes the hand's rank from 9 down to 1, plus the cards in order of “importance” to the scoring of the hand.

The importance of a card depends on the hand. For a straight or straight flush, the most important card is the highest-ranking card. For a full house, the most important cards are the three-of-a kind cards, followed by the pair of cards. For two pair, however, the most important cards are the high-ranking pair, followed by the low-ranking pair. This allows us to compare “two pair 10's and 4's” against “two pair 10's and 9s'”. Both hands have a pair of 10's, meaning we need to look at the third card in order of importance to determine the winner.

Scoring Poker Hands. The Hand class should look like the following. This definition provides a number of methods to check for straight, flush and the patterns of matching cards. These functions are used by the score method, shown below.

class PokerHand:
    def __init__( self, cards ):
        self.cards= cards
        self.rankCount= {}
    def straight( self ):
        
all in sequence

    def straight( self ):
        
all of one suit

    def matches( self ):
        
tuple with counts of each rank in the hand

    def sortByRank( self ):
        
sort into rank order

    def sortByMatch( self ):
        
sort into order by count of each rank, then rank

This function to score a hand checks each of the poker hand rules in descending order.

    def score( self ):
        if self.straight() and self.flush():
            self.sortByRank()
            return 9
        elif self.matches() == ( 4, 1 ):
            self.sortByMatch()
            return 8
        elif self.matches() == ( 3, 2 ):
            self.sortByMatch()
            return 7
        elif self.flush():
            self.sortByRank()
            return 6
        elif self.straight():
            self.sortByRank()
            return 5
        elif self.matches() == ( 3, 1, 1 ):
            self.sortByMatch()
            return 4
        elif self.matches() == ( 2, 2, 1 ):
            self.sortByMatchAndRank()
            return 3
        elif self.matches() == ( 2, 1, 1, 1 ):
            self.sortByMatch()
            return 2
        else:
            self.sortByRank()
            return 1

You'll need to add the following methods to the PokerHand class.

  • straight returns True if the cards form a straight. This can be tackled easily by sorting the cards into descending order by rank and then checking to see if the ranks all differ by exactly one.

  • flush returns True if all cards have the same suit.

  • matches returns a tuple of the counts of cards grouped by rank. This can be done iterating through each card, using the card's rank as a key to the self.rankCount dictionary; the value for that dictionary entry is the count of the number of times that rank has been seen. The values of the dictionary can be sorted, and form six distinct patterns, five of which are shown above. The sixth is simply (1, 1, 1, 1, 1), which means no two cards had the same rank.

  • sortByRank sorts the cards by rank.

  • sortByMatch uses the counts in the self.rankCount dictionary to update each card with its match count, and then sorts the cards by match count.

  • sortByMatchAndRank uses the counts in the self.rankCount dictionary to update each card with its match count, and then sorts the cards by match count and rank as two separate keys.

Exercising Card, Deck and Hand. Once you have the Card, Deck and Hand classes, you can exercise these with a simple function to play one hand of poker. This program will create a Deck and a Hand; it will deal five Cards into the Hand. It can score the hand. It can replace from zero to three cards and score the resulting hand.


 
 
  Published under the terms of the Open Publication License Design by Interspire