Chapter 5: Dice Group
It isn’t uncommon to roll a single die in a table top role playing game. For example, in Dungeons & Dragons rolling a d20
is one of the most common ways to determine the success or failure of a character’s action. However, it is so much more fun (and satisfying) to roll a group of dice!
For example, a common way to create a character is to roll 3 six-sided dice together. The sum of these 3 dice is the resulting ability score. When a person does this, we say they rolled 3d6
.
In this chapter, we will create a DieGroup
class to model a a group of like-sided dice.
Table of contents
00. Create a Feature Branch
You’re about to start a new feature! Before beginning, you should create a feature branch named {username}/dice-group-roller
.
Creating a Branch with GitHub Desktop (Click to Expand)
- Select your project repository from the
Current repository
drop down. - Click the
Current branch
drop down. - Click
New branch
A dialog will open.
- Enter the name of the new branch (e.g.
username/dice-group-roller
) - Click
Create branch
- Publish your branch to GitHub by clicking the
Publish branch
button.
01. The DiceGroup Class
- Create a
DiceGroup
class in yourScripts/Model
folder. - The
DiceGroup
class should be in theAdventureQuest.Dice
name space.
When you’re done, your DiceGroup.cs
file should look match the code below:
namespace AdventureQuest.Dice
{
public class DiceGroup
{
}
}
The DiceGroup
class does not need the UnityEngine
, System.Collections
, or System.Collections.Generic
name spaces.
02. Properties of a DiceGroup
Just as we did with the Die
, let’s first think about the properties that will define a DiceGroup
.
Think about the following questions, then expand the section below.
For example, if you have 3 six-sided dice (3d6
) what can be observed about that group? What about 4d8
? Are there any interesting properties that can be derived from the group that might be useful or interesting? Which should have set
ters?
Did you think through these questions? Did you write down your answers? You will gain so much more from this project if you try to come up with your own solutions first.
DiceGroup Properties (Click to Expand)
One of the best (and worst) parts of programming is that there are many different ways to solve the same problem. This gives you room for creativity! However, it also gives you room to cause yourself (and your team) an infinite amount of pain. That said, if your proposed properties don’t match those in this project, that is okay! On a team, you would have the opportunity to discuss this with your peers, learn, and grow!
I won’t try to claim that the properties I’ve chosen here are the best possible set of properties. But, I have attempted to choose properties that If you came up with something different (or don’t like a choice I’ve made), I’d love to hear about it (you can leave a comment at the bottom of this chapter). Convince me why I should change them. Maybe I’ll update the project!
Properties of a DiceGroup
Below are the properties of the DiceGroup
class that I feel should be exposed publicly:
/// <summary>
/// An array containing each <see cref="Die"/> in this <see cref="DiceGroup"/>
/// </summary>
private readonly Die[] _dice;
/// <summary>
/// The number of dice in this <see cref="DiceGroup"/>
/// </summary>
public int Amount { get; }
/// <summary>
/// The number of sides on each die in this <see cref="DiceGroup"/>.
/// </summary>
public int Sides { get; }
/// <summary>
/// The minimum value that can be rolled by this <see cref="DiceGroup"/>.
/// </summary>
public int Min { get; }
/// <summary>
/// The maximum value that can be rolled by this <see cref="DiceGroup"/>.
/// </summary>
public int Max { get; }
Notice: I have chosen NOT to provide any set
ters for the DiceGroup
.
Additionally, I have chose NOT to include a property which exposes any individual Die
. This was intentional. It could be argued that a DiceGroup
should have an array (or list) containing instances of Die
. So, why did I choose not to include such a property?
I decided that a DiceGroup
should act as a “group” and should only be accessed as a whole. That is, we don’t want to expose the ability to roll an individual die that is part of a DiceGroup
.
That said, I have chosen to create a private readonly Die[]
field _dice
to track the internal state of the DiceGroup
.
03. Derived Properties
Something you may have noticed is that the properties of a DiceGroup
are related. For example, Amount
and Min
should actually be the same value. Why? Consider what happens when you roll the DiceGroup
and all of the dice land on a 1? This is the minimum possible value that can be rolled. It also just so happens to be the amount of dice that are in the group. It could be argued that it doesn’t make sense to have both of these as properties since they are always guaranteed to have the same value.
However, for ease of reasoning, it can be convenient to have a property named Min
. In C#
we can write a get
ter as a derived value using the =>
operator. For example, you can replace:
public int Min { get; }
with
public int Min => Amount;
This is called an Expression body definition and is short hand for writing a method which returns the value of Amount
’s get
ter. It is incredibly useful for defining get
ters that are derived from other fields and properties.
Challenge: Derive each property
Can you derive each of the remaining properties using the _dice
field, another property, or a combination of properties?
Hint 1: Amount
How many dice are in the _dice
array?
Solution
You can determine the number of dice in the array using the Length
property.
public int Amount => _dice.Length;
Hint 2: Sides
Based on the definition of DiceGroup
all of the dice in the _dice
array should have the same number of sides.
Solution
You can access the the first element of the _dice
array and return the number if Sides
it has.
public int Sides => _dice[0].Sides;
Hint 3: Max
What is the maximum value of each die? How many dice are there?
Solution
Sides
represents the maximum value an individual die can roll. If you multiply this with Amount
, you find the maximum possible roll.
public int Max => Amount * Sides;
Challenge: Write a DiceGroup Constructor
Next, define a constructor for DiceGroup
.
- What information is needed to construct a
DiceGroup
? (What parameters will it accept?) - How will you validate the parameters?
Hint 1: Parameters (Click to Expand)
You can define a DiceGroup
with two integers: {amount}d{sides}
Hint 2: Validation (Click to Expand)
- A
DiceGroup
must have at least 1 die - A
Die
must have at least 2 sides
Spoiler: Constructor Declaration and Comment
/// <summary>
/// Instantiates a DiceSet containing <paramref name="amount"/> dice
/// each with the specified number of <paramref name="sides"/>.
/// </summary>
/// <exception cref="System.ArgumentException">
/// If amount is less than 1 or sides is less than 2.
/// </exception>
public DiceGroup(int amount, int sides)
{
if (amount < 1) throw new System.ArgumentException($"DiceGroup must contain at least 1 die but was {amount}.");
if (sides < 2) throw new System.ArgumentException($"DiceGroup must have at least 2 sides but was {sides}.");
// TODO: Initialize _dice
// TODO: Populate _dice
}
DiceGroupTest
With a constructor defined, now would be a good time to add a DiceGroupTest
to your Tests/Model
folder. Below are 2 tests that will help validate your solution to the next challenge:
DiceGroupTest.cs (Click to Expand)
using NUnit.Framework;
namespace AdventureQuest.Dice
{
public class DiceGroupTest
{
[Test, Timeout(5000), Description("Tests the DiceGroup(amount, sides) Constructor")]
public void TestConstructor()
{
DiceGroup group3d6 = new(3, 6);
Assert.AreEqual(3, group3d6.Amount);
Assert.AreEqual(6, group3d6.Sides);
Assert.AreEqual(3, group3d6.Min);
Assert.AreEqual(18, group3d6.Max);
DiceGroup group1d20 = new(1, 20);
Assert.AreEqual(1, group1d20.Amount);
Assert.AreEqual(20, group1d20.Sides);
Assert.AreEqual(1, group1d20.Min);
Assert.AreEqual(20, group1d20.Max);
}
[Test, Timeout(5000), Description("Tests the DiceGroup Constructor validates parameters")]
public void TestConstructorArgumentException()
{
Assert.Throws<System.ArgumentException>(() => new DiceGroup(0, 6));
Assert.Throws<System.ArgumentException>(() => new DiceGroup(-1, 6));
Assert.Throws<System.ArgumentException>(() => new DiceGroup(3, 1));
Assert.Throws<System.ArgumentException>(() => new DiceGroup(3, -1));
Assert.Throws<System.ArgumentException>(() => new DiceGroup(1, 0));
}
}
}
Challenge: Fix the DiceGroup Constructor
Below is an implementation of the DiceGroup
Constructor that initializes the _dice
array to have 3 dice and initializes each of the dice to have 6 sides. Unfortunately, this will not pass the provided unit tests. Can you fix the constructor?
Incomplete Constructor (Click to Expand)
public DiceGroup(int amount, int sides)
{
if (amount < 1) throw new System.ArgumentException($"DiceGroup must contain at least 1 die but was {amount}.");
if (sides < 2) throw new System.ArgumentException($"DiceGroup must have at least 2 sides but was {sides}.");
_dice = new Die[3];
for (int i = 0; i < _dice.Length; i++)
{
_dice[i] = new Die(6);
}
}
Because all of the properties are derived using the _dice
field there is no need to initialize them in the constructor.
Hint 1: Initializing _dice (Click to Expand)
The code above below initializes the _dice
array to have space for 3 Die
objects. How many die objects will you need to store?
_dice = new Die[3];
Hint 2: Populating _dice (Click to Expand)
The code uses for
loop to iterate over the length of the _dice
array (i < _dice.Length
). Each iteration, one of the spaces in the _dice
array is assigned a new 6-sided die (new Die(6)
).
- How many sides should each
Die
have?
for (int i = 0; i < _dice.Length; i++)
{
_dice[i] = new Die(6);
}
Spoiler: Solution
public DiceGroup(int amount, int sides)
{
if (amount < 1) throw new System.ArgumentException($"DiceSet must contain at least 1 die but was {amount}.");
if (sides < 2) throw new System.ArgumentException($"DiceSet must have at least 2 sides but was {sides}.");
_dice = new Die[amount];
for (int i = 0; i < _dice.Length; i++)
{
_dice[i] = new Die(sides);
}
}
Challenge: Add a Roll() method to the
Just like the Die
class the DiceGroup
class will have a Roll()
method which rolls all of the dice in the group and returns the result. To do this, you can write a loop which iterates over all of the dice and calls the _dice[i].Roll()
method. However, you will need to keep track of the each resulting die and sum them together.
Roll() method declaration (Click to Expand)
/// <summary>
/// Rolls all of the dice and returns the sum.
/// </summary>
public int Roll()
{
int sum = 0;
// TODO: Calculate the sum
return sum;
}
Additional Tests (Click to Expand)
You can test your solution using the tests below. Feel free to add additional test as well!
[Test, Timeout(5000), Description("Tests the result of rolling a 3d6 10,000 times.")]
public void TestRoll3d6()
{
DiceGroup group3d6 = new(3, 6);
// Roll the die pool 1000 times ensuring the bounds
int[] values = new int[10_000];
for (int i = 0; i < 10_000; i++)
{
int result = group3d6.Roll();
Assert.LessOrEqual(result, group3d6.Max);
Assert.GreaterOrEqual(result, group3d6.Min);
values[i] = result;
}
// Result should contain all values from 3 to 18
for (int i = group3d6.Min; i <= group3d6.Max; i++)
{
Assert.Contains(i, values);
}
}
[Test, Timeout(5000), Description("Tests the result of rolling a 4d4 10,000 times.")]
public void TestRoll2d4()
{
DiceGroup group2d4 = new(2, 4);
// Roll the die pool 1000 times ensuring the bounds
int[] values = new int[10_000];
for (int i = 0; i < 10_000; i++)
{
int result = group2d4.Roll();
Assert.LessOrEqual(result, group2d4.Max);
Assert.GreaterOrEqual(result, group2d4.Min);
values[i] = result;
}
// Result should contain all values from 3 to 18
for (int i = group2d4.Min; i <= group2d4.Max; i++)
{
Assert.Contains(i, values);
}
}
}
Good Time to Commit
Now would be a good time to make a git
commit. You just finished a feature. More specifically, you just implemented a DiceGroup
class which models a group of like-sided dice.
Committing with GitHub Desktop (Click to Expand)
- Ensure the files you would like to commit are checked in the
Changes
tab.
-
Enter a summary for your commit. Think of this as the subject line of an email. It should be SHORT and to the point. Aim to be less than 50 characters. It is good practice to prefix the commit with the type of work that was done. For example:
- A feature:
feat: Implemented Die class
- A chore:
chore: Added image assets to project
- A bug fix:
fix: Removed off by 1 error
- A work in progress:
wip: Partial implementation of DieGroup class
- A feature:
-
Add a description to your commit. This should provide additional details about what is included in the commit. For example:
This commit adds a Die class which models a multi-sided die providing an
interface with 2 properties: `Sides` and `LastRolled`. Additionally, it provides
a single method: `Roll()` which "rolls" the die and randomly selecting one of
the sides.
Additionally, added unit tests to test the Die class specification.
- When you’re ready, click the
Commit
button
- Lastly, push your commit to GitHub by clicking the
Push origin
button
What’s Next
With a DiceGroup
in place, we can simulate rolling a group of like-sided dice. In [Chapter 6: Dice Analyzer Scene], you will create a scene that allows the player to provide a dice group formula in Dice Notation (3d6
or 2d4
) and roll the DiceGroup
while reporting information about the rolls.
Join the Discussion
If you're stuck, have questions, or want to provide feedback, you can do so below. However, I ask that you please refrain from posting complete solutions to any of the challenges.
Before commenting, you will need to authorize giscus. Alternatively, you can add a comment directly on the GitHub Discussion Board.