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.
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.
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.
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:
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,
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.
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).
Straight Flush. Five cards of adjacent ranks, all of the same suit.
Four of a Kind. Four cards of the same rank, plus another card.
Full House. Three cards of the same rank, plus two cards of the same
rank.
Flush. Five cards of the same suit.
Straight. Five cards of adjacent ranks. In this case, Ace can be
above King or below 2.
Three of a Kind. Three cards of the same rank, plus two cards of other
ranks.
Two Pair. Two cards of one rank, plus two cards of another rank,
plus one card of a third rank.
Pair. Two cards of one rank, plus three cards of other
ranks.
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.
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