CMPS   Spring 2012
Programming Assignment #2: N-sided (Unfair) Die
Due: 11:59pm, Monday, March 5

Requirements

For this assignment, you are to complete the development of a Java class, NSidedDie, similar to that discussed in lecture. (What is provided is only the "skeleton" of the class, which is to say its heading and the signatures of all the public methods. It is left to the student to supply the bodies of those methods, plus any other "infrastructure" needed to complete the class (such as private methods and declarations of instance variables). A tester application, NSidedDieTester, is provided.

As you are no doubt aware, many board games (e.g., Monopoly, Risk) and other games of chance (e.g., craps) employ dice (the plural of "die"), a pair of which is pictured above. The most common type of die is six-sided (which, when tossed, yields an outcome in the range 1..6, according to how many dots are on the side that lands face-up).

Also, most dice are (intended to be) fair, meaning that each outcome is equally likely to occur when the die is tossed. For example, when a fair six-sided die is tossed, each of the outcomes 1 through 6 occurs one-sixth (or 16 2/3 percent) of the time.

Given the various Coin... classes that we've discussed in lecture, it would be very easy to design a class each instance of which acted like a fair six-sided die. The main difference would be that, in the toss() method, we'd make the call rand.nextInt(6) (in order to generate a pseudo-random integer in the range 0..5, which would then be mapped into an outcome in the range 1..6 by adding one) rather than rand.nextBoolean() (where we mapped true into HEADS and false into TAILS).

In order to make your class more interesting (and more generally useful), it is to be capable of having instances that represent N-sided (for any N > 0) and (possibly) unfair dice.

Each constructor in the class will be such that the client can, through the use of an argument passed to the constructor, specify both the number of sides N that the newly-created die is to have and the probability with which each of its N possible outcomes (1 through N) is to occur.

The class will have three constructors, the first of which has this specification:

/** Initializes the die so that it behaves as a fair, six-sided die.
*/
public NSidedDie() { ... } 

In other words, using this constructor produces a fair six-sided die. The second constructor produces a fair die having the number of sides specified by the argument passed to it, which is expected to be a positive integer.

/** Initializes the die so that it behaves as a fair die having
**  the specified number of sides.  (If the specified number is
**  not positive, an IllegalArgumentException is thrown.)
*/
public NSidedDie(int numSides) { ... } 

The third constructor is used to produce a (potentially) unfair die:

/** Initializes the die so that it behaves as one having N sides,
**  where N is the length of the specified array, and where the
**  relative likelihoods of its outcomes are specified by the
**  values in the array elements.
**  (If the array length is zero or any of its elements has a
**  negative value, an IllegalArgumentException is thrown.)
*/
public NSidedDie(double[] relProb) { ... } 

The formal argument name relProb is meant to suggest the phrase "relative probability". The intent here is that, for example, execution of the code segment (in a client of NSidedDie)

double[] f = new double[] { 1.0, 2.0, 0.5, 1.0, 1.5, 2.0 };
NSidedDie die = new NSidedDie(f); 

will end with die's value being (a reference to) a six-sided die that behaves as described in this table:

OutcomeProbability
11/8
21/4
31/16
41/8
53/16
61/4

How did we calculate these probabilities? Each probability is the ratio of one of the array elements to the sum of all of them. More precisely, letting this sum be S, the probability of a die toss resulting in a face value of k is calculated to be f[k-1]/S. (Because die toss outcomes start at one and array indices start at zero, f[k-1] (rather than f[k]) is used in calculating the probability of tossing k.)

In our example, S = 1.0 + 2.0 + 0.5 + 1.0 + 1.5 + 2.0 = 8.0 and f[4] = 1.5, so the ratio f[4]/S is 1.5/8.0, or 3/16. In other words, the ratio between the relative probability of tossing 5 (i.e., f[4], with value 1.5) and the sum of all the relative probabilities (i.e., 8.0) is 3/16, which is why 5 should be tossed three-sixteenths of the time.

Implementation Suggestions

With an unfair die, simulating a toss is more difficult than with a fair die. This section outlines how it can be done.

Suppose that we have an N-sided die, where the intended probabilities of tossing 1, 2, ..., N, are P1, P2, ... PN, respectively. Then we partitition the interval [0,1) into N sub-intervals, which we call I1, I2, ..., IN, such that the length of Ij (for each j) is equal to Pj. To accomplish this, we take the lower bound of I1 to be to be zero and its upper bound to be P1. For each j>1, we take the lower bound of Ij to be the upper bound of Ij-1 and its upper bound to be that value plus Pj. Or, to put it more concisely, we take Ij to be the interval [Qj-1,Qj), where, for all i,

Qi = P1 + P2 + ... + Pi

To toss a die, we call Math.random(), which, as you will recall, returns a pseudo-random number z (of type double) chosen (with approximately uniform distribution) from the interval [0,1). In order to determine the outcome of the die toss, we determine in which of the N sub-intervals z lies. If z lies in I4, for example, the outcome is taken to be 4.

Following our example from above, we would form the six subintervals

I1 = [0, 1/8)  I2 = [1/8, 3/8)  I3 = [3/8, 7/16)  I4 = [7/16, 9/16)  I5 = [9/16, 3/4)  I6 = [3/4, 1)

Seeing the sub-intervals on the number line might help:

  0      1/8             3/8 7/16    9/16        3/4              1
  +-------+---------------+---+-------+-----------+---------------+  
 
  |_______|_______________|___|_______|___________|_______________|  
      I1         I2         I3    I4        I5            I6

Notice that each interval's length is equal to the probability of the corresponding die toss outcome. (E.g., I4 has length two-sixteenths (or one-eighth), corresponding to the intended probability of tossing 4 on the die.)

Question: How do we represent the sub-intervals? Answer: Put their upper bounds into an array of type double[]! For our example, the array (which we'll call upperBounds) would look like this:
012345
1/83/87/169/16 3/41

For the toss() method to simulate a toss of the die, it would do this:

double z = Math.random();

so as to assign to z a pseudo-random number in the interval [0,1). Then it would search in the array upperBounds to find the smallest value of k satisfying the condition

upperBounds[k] > z

The outcome of the die toss (i.e., the resulting face value) would then be k+1.


Program Submission

Submit your source code file (NSidedDie.java) from the course web page using the Submit/Review link that is adjacent to the link that brought you to this page. (Again, submit the .java file, not the .class file.) Make sure to include comments in your program identifying yourself, indicating that it is a solution to Prog. Assg. #2 in CMPS 144, acknowledging any persons who aided you in devloping your solution, and pointing out any flaws of which you are aware.

Be aware that you can submit more than one time. Hence, if, after submitting, you improve your program (e.g., by fixing logic errors), you should submit the newer version.