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:

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.

  1. Use the Throw class as the superclass. A Roulette Bin class doesn’t need a specific list of losing Outcome instances. Indeed, we don’t even need a subclass, since a Roulette Bin instance can just ignore features of the Craps-centric Throw class.

  2. Create a new superclass based on the Bin class. We can then make a Bin subclass that adds no new features. We can change the Throw class to add features to the new superclass. This makes the Bin and Throw 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 a RandomEventFactory instance, a Table instance and have the necessary interface to reset the game and execute one cycle of play. This class is based on the existing Game class, with the Roulette-specific cycle() replaced with an abstract method definition.

  • The concrete RouletteGame subclass. This has the cycle() method appropriate to Roulette that was extracted from the original Game class.

  • The concrete CrapsGame subclass. This has a cycle() method appropriate to Craps. This is a small change to the parent of the CrapsGame 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 the bins 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 the RandomEvent class and include the Bin, an Throw 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 empty Bin instances.

Use the superclass to save the given random number generator instance and invoke initialize().

Methods

Wheel.addOutcome(self, bin: int, outcome: Outcome) → None

Adds the given Outcome to the Bin with the given number.

Wheel.initialize(self) → None

Creates an events collection with the pool of possible events. This will create an instance of BinBuilder, bb, and delegate the construction to the BinBuilder.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 invoke initialize().

Methods

Wheel.addOutcome(self, faces: Tuple[int, int], outcome: Outcome) → None

Adds the given Outcome object to the Throw instance with the given tuple of values. This allows us to create a collection of several one-roll Outcome instances. For example, a throw of 3 includes four one-roll Outcome instances: Field, 3, any Craps, and Horn.

Wheel.initialize(self) → None

Creates the 8 one-roll Outcome instances (2, 3, 7, 11, 12, Field, Horn, Any Craps). It then creates the 36 Throw objects, each of which has the appropriate combination of Outcome instances.

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.

Table.bets

This is a LinkedList of the Bet instances currently active. These will result in either wins or losses to the Player. :noindex:

Table.game

The Game used to determine if a given bet is allowed in a particular game state.

Constructors

Table.__init__(self) → None

Creates an empty list of bets.

Methods

Table.setGame(self, game: Game) → None

Saves the given Game instance to be used to validate bets.

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 from list to any other Collection 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.

Player.table

The Table object used to place individual Bet instances.

Constructors

Player.__init__(self, table: Table) → None

Constructs the Player with a specific Table for placing Bet instances.

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 a stake greater than the table minimum and has a roundsToGo 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 various Bet 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.

Player.win(self, bet: Bet) → None

Notification from the Game that the Bet was a winner. The amount of money won is available via bet.winAmount().

Player.lose(self, bet: Bet) → None

Notification from the Game that the Bet was a loser.

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 or Dice or other subclass of RandomEventFactory that returns a randomly selected RandomEvent with specific Outcome s that win or lose.

Game.table

Contains a CrapsTable or RouletteTable instance which holds all the Bet instances placed by the Player object.

Game.player

Holds the Player object, responsible for placing bets on the Table.

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.

Game.__init__(self, eventFactory: RandomEventFactory, table: Table) → None

Constructs a new Game, using a given RandomEventFactory and Table.

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 call player.placeBets() to get bets. It will call eventFactory.next() to get the next set of Outcome instances. It will then call table.bets() to get an Iterator over the Bet instances. Stepping through this Iterator returns the individual Bet objects. The bets are resolved, calling the Player.win() or Player.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 call player.placeBets() to get bets. It will call wheel.next() to get the next winning Bin. It will then call table.bets() to get an Iterator over the Bet instances. Stepping through this Iterator returns the individual Bet objects. If the winning Bin contains the Outcome, call the Player.win() otherwise call Player.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.

  1. 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.

  2. It will call Dice.next() to get the next winning Throw.

  3. It will use the Throw.updateGame() to advance the game state.

  4. It will then call Table.bets() to get an Iterator; stepping through this Iterator returns the individual Bet objects.

    • It will use the Throw object’s resolveOneRoll() method to check one-roll propositions. If the method returns true, the Bet is resolved and should be deleted.

    • It will use the Throw object’s resolveHardways() method to check the hardways bets. If the method returns true, the Bet 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 current CrapsGameState 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 current CrapsGameState 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 of theThrow, 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 to Throw and Bin. See Design Heavy .

  • Create RandomEventFactory, and associated changes to Wheel and Dice. The existing unit tests will confirm that this change has no adverse effect.

  • Refactor Table and CrapsTable to make a single class of these two. The unit tests for the original CrapsTable should be merged with the unit tests for the original Table.

  • Refactor Player and CrapsPlayer to create a better class hierarchy with CrapsPlayer and RoulettePlayer both sibling subclasses of Player . The unit tests should confirm that this change has no adverse effect.

  • Refactor Game and CrapsGame to create three classes: Game, RouletteGame and CrapsGame. The unit tests should confirm that this change has no adverse effect.

  • Create a new main program class that uses the existing Simulator with the CrapsGame and CrapsPlayer 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.