Dice Class

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.

Dice Overview

The dice have two responsibilities: they are a container for the Throw s and they pick one of the Throws at random.

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,

  • We can rethink our use of a simple sequential structure. If we use a Map, we can use an object representing the pair of numbers as an index instead of a single int value.
  • We have to compute a unique index position from the two dice values.

Decision Forces. There are a number of considerations to choosing between these two representations.

  1. 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 pick one of the existing NumberPair objects at random from the collection of keys for the Map that contains the individual Throws.

  2. 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, i, from two dice, d_1, d_2, via a simple linear equation: i = 6(d_1-1) + (d_2-1).

    We can reverse this calculation to determine the two dice values from an index. d_1 = \lfloor i \div 6 \rfloor + 1; d_2 = (i \bmod 6) + 1.

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.

Throw Rework

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.

We’ll add a method to Throw to return the tuple that is a key for this Throw.

Throw.getKey(self) → tuple

Second, we need to update the Throw constructor to create the key when the Throw is being built. This will allow all parts of the application to share references to a single instance of the key.

NumberPair Design

class NumberPair

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.

Fields

NumberPair.d1
Contains the face of one die.
NumberPair.d2
Contains the face of the other die.

These attributes are built for us by the collections.namedtuple function.

Constructors

The constructor are built for us by the collections.namedtuple function.

Dice Design

class Dice

Dice contains the 36 individual throws of two dice, plus a random number generator. It can select a Throw at random, simulating a throw of the Craps dice.

Fields

Dice.throws
This is a dict that associates a NumberPair with a Throw.
Dice.rng

An instance of random.Random

Generates the next random number, used to select a Throw from the throws collection.

Constructors

Dice.__init__(self, rng=None)

Build the dictionary of Throw instances.

Parameter:rng (random.Random) – The random number generator to use.
At the present time, this does not do the full initialization of all of the Throws. We’re only building the features of Dice related to random selection. We’ll extend this class in a future exercise.

Methods

addThrow(self, throw)
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.
next(self) → Throw

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.

Dice.getThrow(self, d1, d2) → Throw
Parameters:
  • d1 – The value of one die
  • d2 – The other die

While not needed by the application, unit tests may need a method to return a specific Throw rather than a randomly selected Throw.

This method takes a particular combination of dice, locates (or creates) a NumberPair, and returns the appropriate Throw.

Dice Deliverables

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.

    1. Create several instances of Outcome.
    2. Create two instances of Throw that use the available Outcomes.
    3. Create one instance of Dice that uses the two Throws.
    4. A number of calls to the next() method should return randomly selected Throws.

    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.

Dice Optimization

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”

—Donald Knuth

“Structured Programming with Goto Statements”. Computing Surveys 6:4 (1974), 261-301.

In this exercise, it appears that the NumberPair is superfluous.

We can work directly with a list of Throws, bypassing any mapping between NumberPair and Throw.

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.

Table Of Contents

Previous topic

Throw Class

Next topic

Throw Builder Class

This Page