CMPS 134 Fall 2023
Prog. Assg. #7: Spinner
Due: 11:59pm, Friday, December 8

Specification

To the right is an image of a spinner; such devices are used in various board (and other types of) games (e.g., The Game of Life, Twister). The idea is that, during play, a participant makes a spin and the resulting outcome, which is (more or less) random, dictates what happens next in the game. For the spinner in the image, a spin has eight possible outcomes (corresponding to the eight distinct "pie slices"), identified by the integers in the range [1..8].

For this assignment, your primary task is to complete the Java "instance" class Spinner. As its name suggests, each instance of the class represents/simulates a spinner device. Your secondary task is to augment the Java application SpinnerApp that creates an instance of the Spinner class, spins it some number of times, and then reports how many times each possible outcome occurred.

Completing the Spinner Class

The spinner in the image above has eight slices, each apparently of the same size. One can infer that the intent is for all eight possible outcomes to be equally likely to occur. Such a spinner could be referred to as being "fair". One can imagine, however, a spinner whose slices are of different sizes, giving rise to its outcomes having different likelihoods. Such a spinner could be referred to as being "biased".

Our Spinner class is intended to be sufficiently versatile to serve as a model for both fair and biased spinners. When a client program creates a new instance of Spinner, it must specify the number of slices on that spinner. If the intent is for the spinner to be biased, the intended probability of each possible outcome (i.e., the size of each slice) must also be specified. By what means can an instance class make it possible for a client program to specify the characteristics of a newly-created object of that class? Answer: The usual way is by having a constructor that receives that information via its formal parameters.

This raises the question of in what form the intended probabilities of the spinner's outcomes can be represented. The answer adopted by the Spinner class is that those probabilities can be described, indirectly, via an array (of type int[]) of relative probabilities. To illustrate the approach, here is an example:

            0    1    2    3    4
         +----+----+----+----+----+
relProbs |  7 | 12 | 10 |  6 |  5 |
         +----+----+----+----+----+

This array describes a spinner whose five outcomes are intended to occur with probabilities 7/40, 12/40, 10/40, 6/40, and 5/40, respectively. (The numerators come from the values in the array, of course, while the denominator 40 is the sum of those values.) These probabilities translate into 17.5%, 30%, 25%, 15%, and 12.5%, respectively.

The java.util.Random class has a nextInt() method. Suppose that rand is an instance of that class and that N is a positive integer. Then the call rand.nextInt(N) produces a pseudo-random integer in the range [0..N), all with probability 1/N.

Going back to our example, take N to be 40, and consider that we can partition the range of integers [0..40) into five (mutually disjoint) subranges of sizes 7, 12, 10, 6, and 5 (corresponding to the relative probabilities of the five outcomes) like this:


[------|-----------|---------|-----|----)
0      7           19        29    35   40

The subranges are [0..7), [7..19), [19..29), [29..35), and [35..40). The idea is that if, for example, a call to rand.nextInt() produced, say 23, that would be interpeted as a spin resulting in the outcome associated to the subrange [19..29) (because 23 lies in that subrange). Generalizing, the pseudo-random number r would map to the outcome associated to the range [lower..upper) satisfying the condition lower ≤ r < upper.

To implement this approach, it suffices to have an array containing the upper bounds of each of the subranges. Referring to our ongoing example, that array (shown along with the relProbs[] array that would have been received by the constructor) would be like this:

              0    1    2    3    4
           +----+----+----+----+----+
  relProbs |  7 | 12 | 10 |  6 |  5 |
           +----+----+----+----+----+

              0    1    2    3    4
           +----+----+----+----+----+
boundaries |  7 | 19 | 29 | 35 | 40 |
           +----+----+----+----+----+

The relationship between the two arrays is this:

For each k, 0≤k<N, boundaries[k] = sum of the elements in relProbs[0..k].

This relationship is one that is so common that it even has a name: We say that boundaries[] is the prefix sums of relProbs[].

Having used the contents of the relProbs[] array to compute the values in the boundaries[] array (as would have been done by the constructor (or by a method called by the constructor)), the problem (faced by the spin() method) of mapping a pseudo-random number r to an outcome reduces to finding the smallest number k such that r < boundaries[k]. (This is easily accomplished via a sequential search.) Because array index ranges in Java necessarily begin at zero but we are numbering outcomes starting at one, that would translate into a spin whose outcome is k+1.


Completing the SpinnerApp Program

Provided is the Java program SpinnerApp. The table below shows two sample runs of this program, one involving a biased spinner and the other a fair spinner.

Welcome to the Spinner application!

Enter # of slices/outcomes:> 5
Enter 0 for a fair spinner, 1 for biased:> 1
Enter 5 relative probabilities:> 6 3 11 2 5
Enter a seed (for randomness):> 23
Enter number of spins to make:> 120

Outcome 1: 23 times (19.2%)
Outcome 2: 18 times (15.0%)
Outcome 3: 51 times (42.5%)
Outcome 4: 6 times (5.0%)
Outcome 5: 22 times (18.3%)

Goodbye.
Welcome to the Spinner application!

Enter # of slices/outcomes:> 7
Enter 0 for a fair spinner, 1 for biased:> 0
OK; the spinner will be fair.
Enter a seed (for randomness):> 99
Enter number of spins to make:> 60

Outcome 1: 12 times (20.0%)
Outcome 2: 11 times (18.3%)
Outcome 3: 1 times (1.7%)
Outcome 4: 7 times (11.7%)
Outcome 5: 10 times (16.7%)
Outcome 6: 10 times (16.7%)
Outcome 7: 9 times (15.0%)

Goodbye.
Biased Spinner Fair Spinner

You are to make a very minor enhancement to the program so that it outputs not only the number of times that each outcome occurred, but also how many times it was "expected" to occur. For a given outcome, this number is the product of the total number of spins and the probability of that outcome occurring on any one spin. The enhanced program's output is exemplified by the following:

Enhanced SpinnerApp
Welcome to the Spinner application!

Enter # of slices/outcomes:> 5
Enter 0 for a fair spinner, 1 for biased:> 1
Enter 5 relative probabilities:> 6 3 11 2 5
Enter a seed (for randomness):> 23
Enter number of spins to make:> 120

Outcome 1: 23 times (19.2%); Expected was 26.67
Outcome 2: 18 times (15.0%); Expected was 13.33
Outcome 3: 51 times (42.5%); Expected was 48.89
Outcome 4: 6 times (5.0%); Expected was 8.89
Outcome 5: 22 times (18.3%); Expected was 22.22

Goodbye.


Program Submission

Submit your Java source code (in files necessarily named Spinner.java and SpinnerApp.java) to the appropriate Brightspace dropbox. Be sure to complete the "header" comments to identify yourself, any people with whom you collaborated, and any flaws that you know to exist.