Having read about half of the OOAD book, I decided to do an exercise in OO design. Modeling Yatzy and running the simulation was of just about the right size, while simultaneously exercising all of:
- modeling different entities,
- using mutable state,
- advantageous use of inheritance,
- advantageous use of unittesting,
- opportunities for parallelization, profiling & optimization,
- opportunity to play with COM components (instead of shuffling data via text files).
The solution is built with VS 2013 & Office 2013 and consists of the following projects:
- Yatzy
- A self-contained library modeling the abstract game rules, dice and scoring. It also implements a forced rule game. Since the goal of the exercise was practicing OO design, I have not dwelled too much over scoring of corner-cases (e.g., whether full house also could count as two pairs).
- Tests
- A set of unittests. This project needs VS to run as it uses its unittesting framework.
- Simulation
Beginnings of a command-line simulation program. I ended up using it only for performance measurements.
I abandoned in favor ExcelYatzy because it's totally pointless to use intermediate text file for transfering data to Excel when data can be fed to it programatically. This approach has also spared me a lot of time because VS launched Excel automatically, so I didn't need to import a text file manually into Excel each time I ran the simulation.
- ExcelYatzy
- Interactive simulation. This project needs both VS, Office 2013, and VSTO installed to run.
As the Git history shows, I've had a lot of refactorings and design changes. Here, I will describe the major considerations leading to the current design.
The first abstraction that comes to mind are the values of the 5 dice. At first I only
considered storing values, but after a lot of thinking, I came to the conclusion that
position evaluation (e.g., do dice score to a "full house"?) is most easily done by
inspecting count of each value. For example, the count representation of dice values
{4, 3, 1, 4, 4}
is [(1, 1), (3, 1), (4, 3)}
. Internally, an array of 7 integers
is used to represent the count for each value.
The first question was: who owns the values and counts arrays? The DiceState
class
must keep consistent the two views of its data (values and counts), so any silent change
to the underlying raw data must be prohibited. Thus I decided to make it the sole owner
of both views, and keep the two internal arrays private. The class exposed its state
through Counts
and Values
, which were a ReadOnlyCollection
of integers.
The next question was how to set the state when the raw data is private? I decided to
make DiceState
a base class and define a protected method SetState(Action<int[]> setter)
which subclasses would call to set the actual state. The delegate would be passed an
unrelated array to fill in the new state, and SetState
would then perform some
consistency checks (in debug mode only) before copying the new state into the actual
state.
I thought it was a quite good OO design, with protection of private data and controlled
updates. However, I had to give it up in favor of the current design when I realized that
the simulation was running dog-slow, with more than 30% of the total runnint time spent in the copying
part of SetState
. To eliminate copying, I had to give up on the goal of preventing
uncrontolled changes to the actual state. So the Values
and Counts
got public
setters which only copied the reference to the passed array. The caller can now do
the following to make counts and values unsynchronized:
DiceState dice = new DiceState(); int[] values = new int[]{1, 1, 1, 3, 5}; dice.Values = values; // invalidates Counts Debug.Assert(dice.Counts[3] == 1); // lazily computed on access when invalid and kept cached internally values[3] = 4; // XXX! Change not detected, Counts and Values now out of sync
I decided that this design was good enough for my little simulation. Still, the following design question remains: How to programmatically ensure that the caller of a method (or property setter) "loses" a reference to an object once it has made it known to another object? Or, to word it differently: How to ensure that an object is referred to by AT MOST ONE other object?
I encountered a similar performance problem with copying in the EnumeratingDice
class. Copying
was necessary because CompositionGenerator
generated data in an array of 6 elements, but the
Counts
property requires an array of 7 elements (by leaving index 0 unused, some error-prone
index manipulation is simply not necessary). Here I solved the problem by pregenerating all
compositions upon construction (there are only 252) and copying them into a List<int[]>
where every array is of length 7.
Compares two instaces of DiceState
, the from state and the to state. The result of
comparison are two properties:
- DiceToHold
- A
bool[]
property where true positions correspond to the dice that must be changed in an attempt to get from the one to the other position. The returned reference is to a private array. Again, a performance optimization. - Distance
- The number of false values in
DiceToHold
.
Since this was an exercise in OO design, I decided to model position evaluation with an abstract base class. From it, another abstract base classes are derived, with different position evaluation strategies:
- FixedNumberEvaluator
- Evaluates positions ones, twos, ..., sixes. Implements evaluation in terms of an abstract "target number", which is set to a constant by derived classes in their constructor.
- GreedyPatternEvaluator
- Evaluates patterns like "two pairs". Implements greedy evaluation strategy, but the
actual score calculation is delegated to subclasses. It is "greedy" because it tries
to minimize the probability of ending up with a zero score. Thus it chooses the highest-
scoring position within the set of nearest (according to
Distance
) non-zero positions. Number of remaining throws and game rules (e.g., forced or free) are not considered.
This project programatically interacts with Excel in order to run the simulation and display results.
SimData
sheet allows you to simulate a number of games and display results.
Setting either of SEED
or COUNT
fields will simulate the given number of
games and display results. Setting SEED
to a particular value will always
generate the same results. Due to parallel execution, they may be displayed
in different order. Setting SEED
to 0 will set it to the current time of
day and simulate the given number of games.
RefData
sheet displays some statistical properties of the game. The table B2:P32
displays the number of ways each sum in left column can be obtained by the patterns in the
top row (according to the implemented scoring rules). The vector Q2:Q253
displays in
how many ways each of 252 unordered combinations can be achieved by throwing dice where
order matters.
This vector was used to derive the probability of two consecutive throws containing the
same dice, different orderings allowed (0.006353238). During testing, I noticed that
State_Changes_After_Roll
failed because I had set the allowed frequency of two like
consecutive throws at 5/10000, but the test managed to produce ~60 such throws. At first
I suspected a faulty Random
class, but the approximately same frequency was obtained
also with a CSPRNG. Then I proceeded to calculate the theoretical probability and concluded
that everything was in order; with the above probability and 10000 throws one could expect
63 like consecutive throws. Therefore I used the probability threshold of 0.007 to allow
for occasional deviation in the RNG.
During development I hit a curious anomaly in the Excel COM interface: when setting a range of cells, you always have to supply a 2D array, even when the range is 1D, in which case one of the array dimensions has to be 1.