Design Cleanup and Refactoring¶
We have taken an intentionally casual approach to the names chosen for our various classes and the relationships among those classes. At this point, we have a considerable amount of functionality, but it doesn’t reflect our overall purpose, instead it reflects the history of its evolution. This chapter will review the design from Craps and more cleanly separate it from the design for Roulette.
We expect two benefits from the rework in this chapter. First, the design should become “simpler” in the sense that Craps is separated from Roulette, and this will give us room to insert Blackjack into the structure with less disruption in the future. Second, and more important, the class names will more precisely reflect the purpose of the class, making it easier to understand the application. This should make it debug, maintain and adapt.
We’ll start with a review of the current design in Design Review. This will include a number of concerns:
Based on this, we’ll need to rework some existing class defintions. This will involve making small changes to a large number of classes. The work is organized as follows:
In Refactoring Deliverables we’ll detail all of the deliverables for this chapter.
Design Review¶
We can now use our application to generate some more usable results. We
would like the Simulator
class to be able to use our Craps
game, dice, table and players in the same way that we use our Roulette
game, wheel, table and players. The idea would be to give the Simulator
class constructor a bunch of Craps-related objects instead of a bunch of Roulette-related objects
and have everything else work normally. Since we have generally made
Craps a subclass of Roulette, we are reasonably confident that this
should work.
Our Simulator
class constructor requires Game
and Player
instances. Since the CrapsGame
class is a subclass of
the Game
class and the CrapsPlayer
class is a subclass of
the Player
class, we should be able
to construct an instance of Simulator
.
Looking at this, however, we find a serious problem with the names of
our classes and their relationships. When we designed Roulette, we started
out with generic names like Table
, Game
and Player
unwisely. Further, there’s no reason for Craps to be dependent on
Roulette. We would like them to be siblings, and both children of some
abstract game simulation.
We now know enough to factor out the common features of the Game
and CrapsGame
classes to create three new classes from these two.
To find the common features of these two classes,
we’ll see that we have to unify the Dice
and Wheel
classes,
as well as the Table
and CrapsTable
classes and the
Player
and CrapsPlayer
classes.
Looking into Dice
and Wheel
,
we see that we’ll have to tackle
first. Unifying Bin
and Throw
is covered in Design Heavy.
We have several sections on refactoring these various class hierarchies:
The
Bin
andThrow
classes in Unifying Bin and Throw.The
Wheel
Dice
classes in Unifying Dice and Wheel.The
Table
andCrapsTable
classes in Refactoring Table and CrapsTable.The
Player
andCrapsPlayer
classes in Refactoring Player and CrapsPlayer.The
Game
andCrapsGame
classes in Refactoring Game and CrapsGame.
This will give us two properly parallel structures with names that reflect the overall intent.
Unifying Bin and Throw¶
We need to create a common superclass for the Bin
and Throw
classes, so that we can then create some commonality between
the Dice
and Wheel
classes.
The first step, then, is to identify the common features of the Bin
and Throw
classes. The relatively simple Bin
class and the
more complex Throw
class can be unified in one of two ways.
Use the
Throw
class as the superclass. A RouletteBin
class doesn’t need a specific list of losingOutcome
instances. Indeed, we don’t even need a subclass, since a RouletteBin
instance can just ignore features of the Craps-centricThrow
class.Create a new superclass based on the
Bin
class. We can then make aBin
subclass that adds no new features. We can change theThrow
class to add features to the new superclass. This makes theBin
andThrow
classes peers with a common parent.
The first design approach is something we call the Swiss Army Knife
design pattern: create a structure that has every possible feature, and
then ignore the features in subclasses. This creates a distasteful
disconnect between the use of a Bin
instance and the declaration of
the Bin
class:
we only use the set of winning Outcome
instances, but the
object also has a losing set that isn’t used by anything else in the
Roulette game.
We also note that a key feature of OO languages is inheritance, which adds features to a superclass. The Swiss Army Knife design approach, however, works by subtracting features. This creates a distance between the OO language and our design intent.
Our first decision, then, is to refactor the Throw
and Bin
classes
to make them children of a common superclass, which we’ll call the RandomEvent
class.
See the Craps Throw Throw Analysis for our initial thoughts on
this, echoed in the Soapbox on Refectoring sidebar.
The responsibilities for the RandomEvent
class are essentially the
same as the Bin
class. We can then make a Bin
subclass
that doesn’t add any new features, and a Throw
subclass
that adds a number of features, including the value of the two dice and
the set of losing Outcome
instances. See Soapbox on Architecture
for more information on our preference for this kind of design.
Unifying Dice and Wheel¶
When we take a step back from the Dice
and Wheel
classes, we see that they are nearly identical. They
differ in the construction of the Bin
instances or Throw
instances,
but little else. Looking forward, the deck of cards used for
Blackjack is completely different. Craps dice and a Roulette wheel use
selection with replacement: an event is picked
at random from a pool, and is eligible to be picked again any number of
timers. Cards, on the other hand, are selection
without replacement: the cards form a sequence of events of a defined
length that is randomized by a shuffle.
Here’s the consequence. If we have a 5-deck shoe, we can never see more than twenty kings before the shoe is shuffled. However, we can always roll an indefinite number of 7’s on the dice.
We note that there is also a superficial similarity between the rather complex
methods of the BinBuilder
class and the simpler method in the ThrowBuilder
class.
Both work from a simple overall build()
method to create the
collections of Bin
or Throw
objects.
Our second design decision, then, is to create a RandomEventFactory
class
out of the Dice
and Wheel
classes.
Each subclass provides an initialization
method that constructs the the RandomEvent
instances.
When we move on to tackle cards, we’ll have to create a subclass that
uses a different definition of the random choice method, choose()
, and adds shuffle()
.
This will allow a deck of cards to do selection without replacement,
distinct from dice and a wheel which does selection with replacement.
Refactoring Table and CrapsTable¶
We see few differences between the Table
and CrapsTable
classes. When we designed CrapsTable
we had to add a relationship between the CrapsTable
and CrapsGame
objects
so that a table could ask the game to validate individual Bet
instances
based on the state of the game.
If we elevate the CrapsTable
to be the superclass, we
eliminate a need to have separate classes for Craps and Roulette. We are
dangerously close to embracing a Swiss Army Knife design.
The distinction is a matter of degree: one or two
features can be pushed up to the superclass and then ignored in a
subclass.
In this case, both Craps and Roulette can use the Game
as well as the Table
classes to validate bets. This feature will not be ignored
by one subclass. It happens that the Roulette Game will
permit all bets. The point is to push the responsibility into
the Game
instead of the Table
class.
We actually have two sets of rules that must be imposed on bets. The table rules impose an upper (and lower) limit on the bets. The game rules specify which outcomes are legal in a given game state.
The Game
class provides rules as a set of valid Outcome
instances.
The Table
class provides rules via a method that checks the sum of the amount
of the Bet
instances.
Our third design decision is to merge the Table
and CrapsTable
classes
into a new Table
class and use this for both games. This
will simplify the various Game classes by using a single class of Table
for both games.
Refactoring Player and CrapsPlayer¶
Before we can finally refactor the Game
class,
we need to be sure that we have sorted out a proper relationship
between our various players. In this case, we have a large hierarchy,
which will we hope to make even larger as we explore different betting
alternatives. Indeed, the central feature of this simulation is to
expand the hierarchy of players as needed to explore betting strategies.
Therefore, time spent organizing the Player
class hierarchy is
time well spent.
We’d like to have the following hierarchy.
Player.
RoulettePlayer.
RouletteMartingale.
RouletteRandom.
RouletteSevenReds.
Roulette1326.
RouletteCancellation.
RouletteFibonacci.
CrapsPlayer.
CrapsMartingale.
Looking forward to Blackjack, see see that there is much richer player interaction, because there are player decisions that are not related to betting. This class hierarchy doesn’t seems to enable an expansion to separate play decisions from betting decisions. In the case of craps, there seem to be two kinds of betting decisions – outcome choice vs. amount – that isn’t handled very well.
There seem to be at least two “dimensions” to this class hierarchy. One dimension is the game (Craps or Roulette), the other dimension is a betting system (Matingale, 1-3-2-6, Cancellation, Fibonacci, etc.) For Blackjack, there is also a playing system in addition to a betting system. Sometimes this multi-dimensional aspect of a class hierarchy indicates that we should be using multiple inheritance to define our classes.
In the case of Python, we have two approaches for implementation:
Multiple inheritance is part of the language, and we can pursue this directly.
We can also follow the Strategy design pattern to add a betting strategy object to the basic interface for playing the game.
In Roulette there are no game choices. However, in Craps, we made a separated the Pass Line bet, where the payout doesn’t match the actual odds very well, from the Pass Line Odds bet, where the payout does match the odds. This means that a Martingale Craps player really has two betting strategy objects: a flat bet strategy for Pass Line and a Martingale Bet strategy for the Pass Line Odds.
If we separate the player and the betting system, we could mix and match betting systems, playing systems and game rules. In the case of Craps, where we can have many working bets (Pass Line, Come Point Bets, Hardways Bets, plus Propostions), each player would have a mixture of betting strategies used for their unique mixture of working bets.
Rather than fully separate the player’s game interface and betting
system interface, we can try to adjust the class hierarchy and the class
names to those shown above. We need to make the superclass, Player
independent of any game. We can do this by extracting anything
Roulette-specific from the original Player
class and
renaming our Roulette-focused Passenger57
to be RoulettePlayer
,
and fix all the Roulette player subclasses to inherit from RoulettePlayer
.
We will encounter one design difficulty when doing this. That is the
dependency from the various Player1326State
classes on a
field of Player1326
. Currently, we will simply be renaming Player1326
to Roulette1326
. However, as we go forward, we will see
how this small issue will become a larger problem. In Python, we can
easily overlook this, as described in Python and Interface Design.
Refactoring Game and CrapsGame¶
Once we have common RandomEventFactory
,
Table
, and Player
classes, we
can separate the Game
class from the RouletteGame
and CrapsGame
classes to create three new classes:
The abstract superclass,
Game
. This will contain aRandomEventFactory
instance, aTable
instance and have the necessary interface to reset the game and execute one cycle of play. This class is based on the existingGame
class, with the Roulette-specificcycle()
replaced with an abstract method definition.The concrete
RouletteGame
subclass. This has thecycle()
method appropriate to Roulette that was extracted from the originalGame
class.The concrete
CrapsGame
subclass. This has acycle()
method appropriate to Craps. This is a small change to the parent of theCrapsGame
class.
While this appears to be a tremendous amount of rework, it reflects lessons learned incrementally through the previous chapters of exercises. This refactoring is based on considerations that would have been challenging, perhaps impossible, to explain from the outset. Since we have working unit tests for each class, this refactoring is easily validated by rerunning the existing suite of tests.
RandomEventFactory Design¶
Fields¶
-
RandomEventFactory.
rng
¶ The random number generator, a subclass of
random.Random
.Generates the next random number, used to select a
RandomEvent
from thebins
collection.
-
RandomEventFactory.
current
¶ The most recently returned
RandomEvent
.
Constructors¶
-
RandomEventFactory.
__init__
(self, rng: random.Random) → None¶ Saves the given Random Number Generator. Calls the
initialize()
method to create the pool of result instances. These are subclasses of theRandomEvent
class and include theBin
, anThrow
classes.
Methods¶
-
RandomEventFactory.
initialize
(self)¶ Create a collection of
RandomEvent
objects with the pool of possible results.Each subclass must provide a unique implementation for this.
-
RandomEventFactory.
choose
(self) → RandomEvent¶ Return the next
RandomEvent
.Each subclass must provide a unique implementation for this.
Wheel Class Design¶
The Wheel
class is a subclass of the RandomEventFactory
class.
It contains the 38 individual Bin
instances on a Roulette wheel. As a RandomEventFactory
,
it contains a random number generator and can select a Bin
instance
at random, simulating a spin of the Roulette wheel.
Constructors¶
-
Wheel.
__init__
(self, rng: random.Random) → None Creates a new wheel. Create a sequence of the
Wheel.events
with with 38 emptyBin
instances.Use the superclass to save the given random number generator instance and invoke
initialize()
.
Methods¶
-
Wheel.
addOutcome
(self, bin: int, outcome: Outcome) → None¶
-
Wheel.
initialize
(self) → None¶ Creates an
events
collection with the pool of possible events. This will create an instance ofBinBuilder
,bb
, and delegate the construction to theBinBuilder.buildBins()
method.
Dice Class Design¶
The Dice
class is a subclass of the RandomEventFactory
clas.
It contains the 36 individual throws of two dice. As a RandomEventFactory
,
it contains a random number generator and can select a Throw
instance
at random, simulating a throw of the Craps dice.
Constructors¶
-
Dice.
__init__
(self, rng: random.Random) → None Create an empty set of
Dice.events
. Use the superclass to save the given random number generator instance and invokeinitialize()
.
Methods¶
-
Wheel.
addOutcome
(self, faces: Tuple[int, int], outcome: Outcome) → None Adds the given
Outcome
object to theThrow
instance with the given tuple of values. This allows us to create a collection of several one-rollOutcome
instances. For example, a throw of 3 includes four one-rollOutcome
instances: Field, 3, any Craps, and Horn.
Table Class Design¶
The Table
class contains all the Bet
instances created by the Player
.
A table has an association with a Game
, which is
responsible for validating individual bets. A table also has betting
limits, and the sum of all of a player’s bets must be within this limits.
Fields¶
-
Table.
minimum
¶ This is the table lower limit. The sum of a
Player
’s bets must be greater than or equal to this limit.
-
Table.
maximum
¶ This is the table upper limit. The sum of a
Player
’s bets must be less than or equal to this limit.
Constructors¶
-
Table.
__init__
(self) → None Creates an empty
list
of bets.
Methods¶
-
Table.
isValid
(self, bet: Bet) → bool¶ Validates this bet. The first test checks the
Game
to see if the bet is valid.
-
Table.
allValid
(self) → bool¶ Validates the sum of all bets within the table limits. Returns false if the minimum is not met or the maximum is exceeded.
-
Table.
placeBet
(self, bet: Bet) → bool¶ Adds this bet to the list of working bets. If the sum of all bets is greater than the table limit, then an exception should be raised. This is a rare circumstance, and indicates a bug in the
Player
more than anything else.
-
Table.
__iter__
(self) → Iterator[Bet]¶ Returns an
Iterator
over the list of bets. This gives us the freedom to change the representation fromlist
to any otherCollection
with no impact to other parts of the application.We could simply return the list object itself. This may, in the long run, prove to be a limitation. It’s handy to be able to simply iterate over a table and example all of the bets.
-
Table.
__str__
(self) → str¶ Reports on all of the currently placed bets.
Player Class Design¶
The Player
class places bets in a game. This an
abstract class, with no actual body for the placeBets()
method. However, this class does implement the basic win()
and
lose()
methods used by all subclasses.
Roulette Player Hierarchy. The classes in the Roulette Player
hierarchy need to have their superclass adjusted to conform to the
newly-defined superclass. The former Passenger57
class is renamed to
RoulettePlayer
. All of the various Roulette players become
subclasses of the new RoulettePlayer
class.
In addition to renaming the Player1326
class to Roulette1326
,
we will also have to change the references in the various classes of the
Player1326State
class hierarchy. We suggest leaving the
class names alone, but merely changing the references within those five
classes from Player1326
to Roulette1326
.
Craps Player Hierarchy. The classes in the Craps Player hierarchy need
to have their superclass adjusted to conform to the newly-defined
superclass. We can rename CrapsPlayerMartingale
to CrapsMartingale
,
and make it a subclass of CrapsPlayer
. Other than names,
there should be no changes to these classes.
Fields¶
-
Player.
stake
¶ The player’s current stake. Initialized to the player’s starting budget.
-
Player.
roundsToGo
¶ The number of rounds left to play. Initialized by the overall simulation control to the maximum number of rounds to play. In Roulette, this is spins. In Craps, this is the number of throws of the dice, which may be a large number of quick games or a small number of long-running games. In Craps, this is the number of cards played, which may be large number of hands or small number of multi-card hands.
Constructors¶
Methods¶
-
Player.
playing
(self) → bool¶ Returns
True
while the player is still active. There are two reasons why a player may be active. Generally, the player has astake
greater than the table minimum and has aroundsToGo
greater than zero. Alternatively, the player has bets on the table; this will happen in craps when the game continues past the number of rounds budgeted.
-
Player.
placeBets
(self) → Bool¶ Updates the
Table
with the variousBet
instances.When designing the
Table
, we decided that we needed to deduct the amount of a bet from the stake when the bet is created. See the Table Roulette Table Analysis for more information.
Game Class Design¶
An instance of the Game
class manages the sequence of actions that defines casino
games, including Roulette, Craps and Blackjack. Individual subclasses
implement the detailed playing cycles of the games. This superclass has
methods for notifying the Player
instance to place bets, getting a
new RandomEvent
instance
and resolving the Bet
objectss actually present on the Table
instance.
Fields¶
-
Game.
eventFactory
¶ Contains a
Wheel
orDice
or other subclass ofRandomEventFactory
that returns a randomly selectedRandomEvent
with specificOutcome
s that win or lose.
Constructors¶
We based this constructor on an design that allows any of these objects to be replaced. This is the Strategy (or Dependency Injection) design pattern. Each of these objects is a replaceable strategy, and can be changed by the client that uses this game.
Additionally, we specifically do not include the Player
instance in the constructor. The Game
instance exists
independently of any particular Player
object.
Methods¶
-
Game.
cycle
(self, player: Player) → None¶ This will execute a single cycle of play with a given
Player
. For Roulette is is a single spin of the wheel. For Craps, it is a single throw of the dice, which is only one part of a complete game. This method will callplayer.placeBets()
to get bets. It will calleventFactory.next()
to get the next set ofOutcome
instances. It will then calltable.bets()
to get anIterator
over theBet
instances. Stepping through thisIterator
returns the individualBet
objects. The bets are resolved, calling thePlayer.win()
orPlayer.lose()
.
-
Game.
reset
(self) → None¶ As a useful default for all games, this will tell the table to clear all bets. A subclass can override this to reset the game state, also.
RouletteGame Class Design¶
The RouletteGame
is a subclass of the Game
class that
manages the sequence of actions that defines the game of Roulette.
Methods¶
-
RouletteGame.
cycle
(self, player: Player) → None¶ This will execute a single cycle of the Roulette with a given
Player
instance. It will callplayer.placeBets()
to get bets. It will callwheel.next()
to get the next winningBin
. It will then calltable.bets()
to get anIterator
over theBet
instances. Stepping through thisIterator
returns the individualBet
objects. If the winningBin
contains theOutcome
, call thePlayer.win()
otherwise callPlayer.lose()
.
CrapsGame Class Design¶
The CrapsGame
is a subclass of the Game
class that manages
the sequence of actions that defines the game of Craps.
Note that a single cycle of play is one throw of the dice, not a complete craps game. The state of the game may or may not change.
Methods¶
-
RouletteGame.
cycle
(self, player: Player) → None This will execute a single cycle of play with a given
Player
.It will call
Player.placeBets()
to get bets. It will validate the bets, both individually, based on the game state, and collectively to see that the table limits are met.It will call
Dice.next()
to get the next winningThrow
.It will use the
Throw.updateGame()
to advance the game state.It will then call
Table.bets()
to get anIterator
; stepping through thisIterator
returns the individualBet
objects.It will use the
Throw
object’sresolveOneRoll()
method to check one-roll propositions. If the method returns true, theBet
is resolved and should be deleted.It will use the
Throw
object’sresolveHardways()
method to check the hardways bets. If the method returns true, theBet
is resolved and should be deleted.
-
CrapsGame.
pointOutcome
(self) → Outcome Returns an
Outcome
instance based on the current point. This is used to create Pass Line Odds or Don’t Pass Odds bets. This delegates the real work to the currentCrapsGameState
object.
-
CrapsGame.
moveToThrow
(self, bet: Bet, throw: Throw) → None Moves a Come Line or Don’t Come Line bet to a new
Outcome
based on the current throw. This delegates the move to the currentCrapsGameState
object.This method should – just as a precaution – assert that the value of
theThrow
is 4, 5, 6, 8, 9 or 10. These point values indicate that a Line bet can be moved. For other values oftheThrow
, this method should raise an exception, since there’s no reason for attempting to move a line bet on anything but a point throw.
-
CrapsGame.
reset
(self) → None This will reset the game by setting the state to a new instance of the
GamePointOff
class. It will also tell the table to clear all bets.
Refactoring Deliverables¶
There are six deliverables for this exercise.
If necessary, create
RandomEvent
, and revisions toThrow
andBin
. See Design Heavy .Create
RandomEventFactory
, and associated changes toWheel
andDice
. The existing unit tests will confirm that this change has no adverse effect.Refactor
Table
andCrapsTable
to make a single class of these two. The unit tests for the originalCrapsTable
should be merged with the unit tests for the originalTable
.Refactor
Player
andCrapsPlayer
to create a better class hierarchy withCrapsPlayer
andRoulettePlayer
both sibling subclasses ofPlayer
. The unit tests should confirm that this change has no adverse effect.Refactor
Game
andCrapsGame
to create three classes:Game
,RouletteGame
andCrapsGame
. The unit tests should confirm that this change has no adverse effect.Create a new main program class that uses the existing
Simulator
with theCrapsGame
andCrapsPlayer
classes.
Looking Forward¶
Now that we have a more organized and symmetric class hierarchy, we can look again again at the variety of play options available in Craps. In the next chapter, we’ll implement a number of simple Craps players with different strategies.