Skip to main content Link Search Menu Expand Document (external link)

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
  1. 00. Create a Feature Branch
  2. 01. The DiceGroup Class
  3. 02. Properties of a DiceGroup
  4. 03. Derived Properties
  5. Challenge: Derive each property
  6. Challenge: Write a DiceGroup Constructor
  7. Challenge: Fix the DiceGroup Constructor
  8. Challenge: Add a Roll() method to the
  9. Good Time to Commit
  10. What’s Next

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)

  1. Select your project repository from the Current repository drop down.
  2. Click the Current branch drop down.
  3. Click New branch

Create New Branch

A dialog will open.

  1. Enter the name of the new branch (e.g. username/dice-group-roller)
  2. Click Create branch

Name New Branch

  1. Publish your branch to GitHub by clicking the Publish branch button.

Publish New Branch

01. The DiceGroup Class

  1. Create a DiceGroup class in your Scripts/Model folder.
  2. The DiceGroup class should be in the AdventureQuest.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 setters?

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 setters 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 getter 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 getter. It is incredibly useful for defining getters 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)

  1. Ensure the files you would like to commit are checked in the Changes tab.

Check the Files to Commit

  1. 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
  2. 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.
  1. When you’re ready, click the Commit button

Add Description

  1. Lastly, push your commit to GitHub by clicking the Push origin button

Push to Origin

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.