Unlike Roulette, where a single Bin could be identified by the number in the bin, dice use a pair of numbers. In this chapter, we design Dice, as well as designing an inner class that is used only to make a single key out of a composite object.
We find that we have a potential naming problem: both a Wheel and the Dice are somehow instances of a common abstraction. Looking forward, we may wind up wrestling with a deck of cards trying to invent a common nomenclature for the classes. They create random events, and this leads us to a possible superclass: Randomizer. Rather than over-engineer this, we’ll hold off on adding this design element until we find something else that is common among them. See Soapbox on Over-Engineering for some additional opinions on this subject.
Container. Since the Dice have 36 possible Throws, it is a collection. We can review our survey of the collections in Design Decision – Choosing A Collection for some guidance here. In this case, we note that the choice of Throw can be selected by a random numeric index.
For Python programmers, this makes the a list very appealing.
After selection a collection type, we must then deciding how to index each Throw in the Dice collection. Recall that in Roulette, we had 38 numbers: 1 to 36, plus 0 and 00. By using 37 for the index of the Bin that contained 00, we had a simple integer index for each Bin.
For Craps it seems better to use a two-part index with the values of two independent dice.
Index Choices. In this case, we have two choices for computing the index into the collection,
Decision Forces. There are a number of considerations to choosing between these two representations.
If we create a new class to contain each unique pair of integers, we can then use that object to be the index for a Map. The Map associates this “pair of numbers” object with its Throw.
In Python, a tuple does this job nicely. We don’t need to define a whole new class.
We can transform the two numeric dice values to a single index value for the sequence. This is a technique called Key Address Transformation; we transform the keys into the address (or index) of the data.
We create the index, , from two dice, , , via a simple linear equation: .
We can reverse this calculation to determine the two dice values from an index. .
Because of encapsulation, the choice of algorithm is completely hidden within the implementation of Dice.
While the numeric calculation seems simple, it doesn’t obviously scale to larger collections of dice very well. While Craps is a two-dice game, we can imagine simulating a game with larger number of dice, making this technique complex.
Solution. Our recommendation is to encapsulate the pair of dice in a tuple instance. We can use this object as index into a dict that associates a tuple with a Throw.
More advanced students can create a class hierarchy for Dice that includes all of the various implementations as alternative subclasses.
Random Selection. The random number generator in random.Random helps us locate a Throw at random.
First, we can get the list of keys from the dict that associates a tuple of dice numbers with a Throw.
Second, we use Random.choice() to pick one of these tuples.
We use this randomly selected tuple to return the selected Throw.
We need to update Throw to return an appropriate key object.
There are two parts to this. First, we need a “getter” to return the key.
In Python, it’s often easiest to use a “named tuple” rather than use a simple tuple.
from collections import namedtuple NumberPair = namedtuple('NumberPair', ['d1','d2'])
This can make the tuple slightly easier to work with.
These attributes are built for us by the collections.namedtuple function.
The constructor are built for us by the collections.namedtuple function.
An instance of random.Random
Generates the next random number, used to select a Throw from the throws collection.
Build the dictionary of Throw instances.
|Parameter:||rng (random.Random) – The random number generator to use.|
|Parameter:||throw (Throw) – The Throw to add.|
Adds the given Throw to the mapping maintained by this instance of Dice. The key for this Throw is available from the Throw.getKey() method.
Returns the randomly selected Throw.
First, get the list of keys from the throws.
The random.Random.choice() method will select one of the available keys from the the list.
This is used to get the corresponding Throw from the throws Map.
There are three deliverables for this exercise. In considering the unit test requirements, we note that we will have to follow the design of the Wheel class for convenient testability: we will need a way to get a particular Throw from the Dice, as well as replacing the random number generator with one that produces a known sequence of numbers.
The Dice class.
A class which performs a unit test of building the Dice class. The unit test should create several instances of Outcome, two instances of Throw, and an instance of Dice. The unit test should establish that Throws can be added to the Dice.
A class which performs a demonstration of selecting non-random values from the Dice class. By setting a particular seed, the Throws will be returned in a fixed order. To discover this non-random order, a demonstration should be built which includes the following.
Note that the sequence of random numbers is fixed by the seed value. The default constructor for a random number generator creates a seed based on the system clock. If your unit test sets a particular seed value, you will get a fixed sequence of numbers that can be used to get a consistent result.
First, we note that premature optimization is a common trap.
“We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%. A good programmer will not be lulled into complacency by such reasoning, he will be wise to look carefully at the critical code; but only after that code has been identified”
“Structured Programming with Goto Statements”. Computing Surveys 6:4 (1974), 261-301.
In this exercise, it appears that the NumberPair is superfluous.
Removing this needless code is an exercise that should be considered, but not done at this time. In the Design Cleanup and Refactoring chapter, we’ll clean up this design to remove several things which – in retrospect – will be poor design decisions.