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, we expect the design to 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 system, which means it will be easier to debug, maintain and adapt.
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‘s constructor Craps-related objects instead 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.
Looking at this, however, we find a serious problem with the names of our classes and their relationships. When we designed Roulette, we used 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 Game and CrapsGame to create three new classes from these two. However, to find the common features of these two classes, we’ll see that we have to unify Dice and Wheel, as well as Table and CrapsTable and Player and CrapsPlayer.
Looking into Dice and Wheel, we see that we’ll have to tackle Bin and Throw first. Unifying Bin and Throw is covered in Design Heavy. If the necessary design for RandomEvent is already in place, skip to Unifying Dice and Wheel. After that, we can refactor the other classes.
We need to create a common superclass for Bin and Throw, so that we can then create some commonality between Dice and Wheel. This unification will make the Vector of results and the next() method identical.
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 you don’t need. This creates a distasteful disconnect between the use of Bin and the declaration of Bin: we only use a the Set of winning Outcomes, but the object has the 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. Since the languages we’re using don’t really support class definition by subtraction, we try to ignore the features, creating a distance between the OO language and our design intent.
Our first decision, then, is to refactor Throw and Bin to make them instances of a common superclass, which we’ll call RandomEvent. See the Craps Throw Throw Overview for our initial thoughts on this, echoed in the Soapbox on Refectoring sidebar.
The responsibilities for RandomEvent are essentially the same as Bin. We can then make Bin a subclass that doesn’t add any new features, and Throw a subclass that adds a number of features, including the value of the two dice and the Set of losing Outcome s. Note that we have made Throw and Bin siblings of a common superclass. See Soapbox on Architecture for more information on our preference for this kind of design.
When we take a step back from Dice and Wheel, we see that they are nearly identical. They differ in the construction of the Bins or Throws, but little else. Looking forward, the deck of cards used for Blackjack is completely different. Dice and a Roulette wheel use :phrase:`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. If we have a 5-deck shoe, we can only see five kings of spades during the game, and we only have 260 cards. However, we can roll an indefinite number of 7’s on the dice.
We note that there is a superficial similarity between the rather complex BinBuilder methods and the simpler method in ThrowBuilder. However, there is no compelling reason for polymorphism between these two classes. We don’t have to factor these into a common class hierarchy.
Our second design decision, then, is to create a RandomEventFactory out of Dice and Wheel . This refactoring will make the Vector of results and the next() method part of the superclass. Each subclass provides the initialization method that constructs the Vector of RandomEvents.
When we move on to tackle cards, we’ll have to create a subclass that uses a different definition of next() and adds shuffle(). This will allow a deck of cards to do selection without replacement.
We see few differences between Table and CrapsTable. When we designed CrapsTable we had to add a relationship between CrapsTable and CrapsGame so that a table could ask the game to validate individual Bets 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 may appear to be merely a matter of degree: one or two features can be pushed up to the superclass and then ignored in a subclass. However, in this case, both Craps and Roulette will use the Game to validate bets: the feature will not be ignored. It happens that the Roulette Game will permit all bets, but we have made that the Game’s responsibility, not the Table’s. Indeed, viewed this way, the Roulette version of Table implicitly took a responsibility away from the Roulette Game because the Table failed to collaborate with the Game for any game-state specific rules. At the time, we overlooked this nuance because we knew that Roulette was stateless and we were comfortable making that assumption part of the design.
Our third design decision is to merge Table and CrapsTable 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.
Before we can finally refactor Game, 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 far 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 hierarchy is time well spent.
We’d like to have the following hierarchy.
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 seems to enable that kind of expansion.
We note that there are two “dimensions” to this class hierarchy. One dimension is the game (Craps or Roulette), the other dimension is a betting system (Matingale, 1326, Cancellation, Fibonacci). For Blackjack, there is also a playing system in addition to a betting system. Sometimes this multi-dimensionsal aspect of a class hierarchy indicates that we could be using multiple inheritance. In the case of Python, we have multiple inheritance in the language, and we can pursue this directly. Java, however, demands the Strategy design pattern to add a flexible betting strategy object to the basic interface for playing the game.
In Roulette, where we are placing a single bet, there is almost no distinction between game interface and the betting system. However, in Craps, we made a distinction in our Martingale player by separating their Pass Line bet (where the payout doesn’t match the actual odds very well) from their Pass Line Odds bet (where the payout does match the odds). This means that our Martingale Craps player really has two betting strategies 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 easily 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. This leads to an interesting issue in the composition of such a complex object. For the current exercise, however, we won’t formally separate the player from the various betting strategies.
Rather than fully separate the player’s game interface and betting system interface, we can simply 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.
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 is a superclass for Dice, Wheel, Cards, and other casino random-event generators.
The random number generator, a subclass of random.Random.
Generates the next random number, used to select a RandomEvent from the bins collection.
Saves the given Random Number Generator. Calls the initialize() method to create the vector of results.
Create a collection of RandomEvent objects with the pool of possible results.
Each subclass must provide a unique implementation for this.
Return the next RandomEvent.
Each subclass must provide a unique implementation for this.
Wheel is a subclass of RandomEventFactory that contains the 38 individual Bins on a Roulette wheel. As a RandomEventFactory, it contains a random number generator and can select a Bin at random, simulating a spin of the Roulette wheel.
Creates a new wheel. Create a sequence of the Wheel.events with with 38 empty Bins.
Use the superclass to save the given random number generator instance and invoke initialize().
Creates the events vector with the pool of possible events. This will create an instance of BinBuilder, bb, and delegate the construction to the buildBins() method of the bb object.
Dice is a subclass of RandomEventFactory that contains the 36 individual throws of two dice. As a RandomEventFactory, it contains a random number generator and can select a Throw at random, simulating a throw of the Craps dice.
Create an empty set of Dice.events. Use the superclass to save the given random number generator instance and invoke initialize().
Adds the given Outcome to the Throw with the given NumberPair. This allows us to create a collection of several one-roll Outcomes. For example, a throw of 3 includes four one-roll Outcomes: Field, 3, any Craps, and Horn.
Table contains all the Bets 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.
Saves the given Game to be used to validate bets.
Validates this bet. The first test checks the Game to see if the bet is valid.
Validates the sum of all bets within the table limits. Returns false if the minimum is not met or the maximum is exceeded.
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 thrown (Java) or raised (Python). This is a rare circumstance, and indicates a bug in the Player more than anything else.
Returns an Iterator over the list of bets. This gives us the freedom to change the representation from LinkedList to any other Collection with no impact to other parts of the application.
In Python, similarly, we can return an iterator over the available list of Bet instances. The more traditional Python approach is to return the list itself, rather than an iterator over the list. With the introduction of the generators in Python 2.3, however, it is slightly more flexible to return an iterator rather than the collection itself. The iterator could be built, for example, using the yield statement instead of the iter() function.
Reports on all of the currently placed bets.
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 is renamed to RoulettePlayer. All of the various Roulette players become subclasses of RoulettePlayer.
In addition to renaming Player1326 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 CrapsPlayerMartigale to CrapsMartigale, and make it a subclass of CrapsPlayer. Other than names, there should be no changes to these classes.
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.
Notification from the Game that the Bet was a winner. The amount of money won is available via bet.winAmount().
Game 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 to place bets, getting a RandomEvent and resolving the Bet s actually present on the Table.
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 exists independently of any particular Player, and we defer binding the Player and Game until we are gathering statistical samples.
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 Outcomes. It will then call table.bets() to get an Iterator over the Bets. Stepping through this Iterator returns the individual Bet objects. The bets are resolved, calling the thePlayer win(), otherwise call the thePlayer lose().
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 is a subclass of Game that manages the sequence of actions that defines the game of Roulette.
This will execute a single cycle of the Roulette with a given Player. 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 Bets. Stepping through this Iterator returns the individual Bet objects. If the winning Bin contains the Outcome, call the thePlayer win(), otherwise call the thePlayer lose().
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.
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 winning Throw.
- It will use the throw.updateGame() to advance the game state.
- It will then call table.bets() to get an Iterator; stepping through this Iterator returns the individual Bet objects.
Returns the Outcome 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.
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.
This will reset the game by setting the state to a new instance of GamePointOff. It will also tell the table to clear all bets.
There are six deliverables for this exercise.