Lesson 3.4: Random Dice Roller

Many game systems have a way of producing randomized results. Be it selecting a random number in some range (min-max), flipping coins, or rolling dice. For RPGs, rolling dice is a seminal concept — you’ve seen it in all the RPGs that you’ve likely played. It is burned into the genre by the popularity of games like Dungeon & Dragons (D&D). For our SimpleRPG game, we are also going to simulate random behaviors by rolling dice.

Installing D20Tek.DiceNotation Package

Since dice rolling and notation is such a base concept in many games and tools, I have previously created a dice rolling NuGet package. We are going to use this package in our SimpleRPG game to roll dice and simulate randomized selection, attacks, and damage.

We are going to start by installing the D20Tek.DiceNotation package, as we did with other libraries earlier (Blazorise, bUnit, etc). Let’s launch the NuGet package manager, by right clicking on SimpleRPG.Game.Engine project and selecting the Manage NuGet Packages menu item.

Fig 1 – NuGet search for D20Tek.DiceNotation.Standard

Then, let’s search for D20Tek.DiceNotation.Standard (this is a .NET Standard 2.1 library so it can be used in Blazor client projects — there are other versions for .NET Core and full .NET Framework 4.7). Select the DiceNotation package. And then click the Install button. If any dialogs come up during the installation, just complete by selecting Ok.

Using D20Tek.DiceNotation

The main interfaces that we will use from the DiceNotation library are: IDice and IDieRoller. IDice is used to define the number of dice, type of dice, and modifiers, and perform the Roll operation. IDieRoller has 3 different implementations and performs the random roll for each specified die.

D20Tek.DiceNotation has a couple of different modes which it can be used in depending on how we want to build up the dice expression:

  1. Programmatically: We can build up the dice to roll by coding the various parts that make up a dice expression. The expression can be built by chaining together operations (as a Fluent API style).
IDice dice = new Dice();
// equivalent of dice expression: 4d6k3 + d8 + 5
dice.Dice(6, 4, choose: 3).Dice(8).Constant(5);
DiceResult result = dice.Roll(new RandomDieRoller());
Console.WriteLine("Roll result = " + result.Value);

dice.Clear()
result = dice.Dice(6, 1).Roll(new RandomDieRoller());
Console.WriteLine("Roll result = " + result.Value);

The first code block creates a complex dice expression with multiple dice types and a modifier to show that very complex expressions can be built programmatically. The second block of code just randomly picks a number from 1-6 (rolls one six-side die). Finally, the Roll method is what actually performs the random generation.

  1. Dice Notation String: We can also create the dice expression by parsing a string that follows the Dice Notation language (also see: examples of dice notations). When we parse the text, we create a similar expression tree as the programmatic version, and it is then evaluated.
IDice dice = new Dice();
DiceResult result = dice.Roll("d20+4", new RandomDieRoller());
Console.WriteLine("Roll result = " + result.Value);

This code just uses the notation string in the Roll method call. This overload of the Roll method knows how to parser the string and roll the resulting expression.

DiceResult is the class returned from the Roll operations. It consists of the following definition, so that we not only get the final value of the roll, but also look at each individual die result (if we need that detail) and the dice expression rolled… among other things.

namespace D20Tek.DiceNotation
{
    /// <summary>
    /// Class the represents the results for any dice expression calculation.
    /// </summary>
    public class DiceResult
    {
        /// <summary>
        /// Gets or sets the dice expression that was evaluated.
        /// </summary>
        public string DiceExpression { get; set; }

        /// <summary>
        /// Gets or sets the die roller used to generate this result.
        /// </summary>
        public string DieRollerUsed { get; set; }

        /// <summary>
        /// Gets the display text representation of the Results list.
        /// </summary>
        [JsonIgnore]
        public string RollsDisplayText
        {
            get
            {
                if (this.Results == null)
                {
                    return string.Empty;
                }

                return Converter.Convert(new List<TermResult>(this.Results), typeof(string), null, "en-us") as string;
            }
        }

        /// <summary>
        /// Gets or sets the results list for all of the terms in this dice expression.
        /// </summary>
        public IReadOnlyList<TermResult> Results { get; set; }

        /// <summary>
        /// Gets or sets the numeric value for this result.
        /// </summary>
        public int Value { get; set; }
    }
}

We will use both dice rolling methods in our game engine, but the dice notation string is a concise way to define expected dice rolls, so we will primarily use that method.

Dice Rollers:

Both of the usage options above use the RandomDieRoller, which uses the .NET Random class to produce the random dice rolls. There are additional die rollers that are included in this library:

  • ConstantDieRoller – lets us create a roller that always returns the same value (specified in its constructor). This roller is great for testing features and expressions because the results will be consistent in our unit tests.
  • CryptoDieRoller – uses Cryptography API to create a more random number generator.
  • MathNetDieRoller – provides various strategies for random number generators to produce our die rolls.

Finally, the library defines the IDieRoller interface that developers can use to build their own custom die rollers. If the random number generators in this library don’t suffice, developers can override the behavior with their own random number generation.

Game Engine DiceService

In our game, we are going to create a DiceService that is a singleton object to be used throughout the game engine. We don’t want to create IDice and IDieRoller objects all over our project, in every class that may need to roll a random number. So we will build the DiceService to provide that layer of abstraction.

For those unfamiliar with it, the Singleton design pattern involves a single class which is responsible to create an object while making sure that only a single object gets created. This class provides a property to access that only object, which is static so that it can be accessed directly without the need to instantiate instances of the class.

To implement our DiceService singleton, we will start by defining an interface for our service. In the SimpleRPG.Game.Engine, create a new folder named Services – then create the IDiceService interface:

using D20Tek.DiceNotation;
using D20Tek.DiceNotation.DieRoller;

namespace SimpleRPG.Game.Engine.Services
{
    public interface IDiceService
    {
        public enum RollerType
        {
            Random = 0,
            Crypto = 1,
            MathNet = 2
        }

        IDice Dice { get; }

        IDiceConfiguration Configuration { get; }

        IDieRollTracker? RollTracker { get; }

        void Configure(RollerType rollerType, bool enableTracker = false);

        DiceResult Roll();

        DiceResult Roll(string diceNotation);
    }
}

Now let’s implement that interface in the DiceService class below (also created in the Services folder):

using D20Tek.DiceNotation;
using D20Tek.DiceNotation.DieRoller;
using System;

namespace SimpleRPG.Game.Engine.Services
{
    public class DiceService : IDiceService
    {
        private static readonly IDiceService _instance = new DiceService();

        /// <summary>
        /// Make constructor private to implement singletone pattern.
        /// </summary>
        private DiceService()
        {
        }

        /// <summary>
        /// Static singleton property
        /// </summary>
        public static IDiceService Instance => _instance;

        //--- IDiceService implementation

        public IDice Dice { get; } = new Dice();

        public IDieRoller DieRoller { get; private set; } = new RandomDieRoller();

        public IDiceConfiguration Configuration => Dice.Configuration;

        public IDieRollTracker? RollTracker { get; private set; } = null;

        public void Configure(IDiceService.RollerType rollerType, bool enableTracker = false)
        {
            RollTracker = enableTracker ? new DieRollTracker() : null;

            DieRoller = rollerType switch
            {
                IDiceService.RollerType.Random => new RandomDieRoller(RollTracker),
                IDiceService.RollerType.Crypto => new CryptoDieRoller(RollTracker),
                IDiceService.RollerType.MathNet => new MathNetDieRoller(RollTracker),
                _ => throw new ArgumentOutOfRangeException(nameof(rollerType)),
            };
        }

        public DiceResult Roll() => Dice.Roll(DieRoller);

        public DiceResult Roll(string diceNotation) => Dice.Roll(diceNotation, DieRoller);
    }
}

This service class simplifies our use of the DiceNotation library by providing the following functionality:

  • The Singleton pattern hides the class constructor and provides a static property to get the single instance of this class.
  • Provides properties to get IDice and current IDieRoller.
  • Configure method lets user pick which die rollers to use. The service defaults to the RandomDieRoller, when this method is not called.
  • Roll methods are pass through calls to the corresponding IDice methods to generate random rolls.

In the game engine code, we will call IDiceService.Roll("1d20") to do random number generation for treasure creation, combat, and damage.

Notice the switch code block (lines 37-44) in the Configure method. It uses the new format for a simple switch expression. That switch expression is the equivalent of the following code:

    switch (rollerType)
    {
        case IDiceService.RollerType.Random:
            DieRoller = new RandomDieRoller(RollTracker);
            break;
        case IDiceService.RollerType.Crypto:
            DieRoller = new CryptoDieRoller(RollTracker);
            break;
        case IDiceService.RollerType.MathNet:
            DieRoller = new MathNetDieRoller(RollTracker);
            break;
        default:
            throw new ArgumentOutOfRangeException(nameof(rollerType));
    }

Now that we have the DiceService using the DiceNotation library, we are ready to start using it for our random number generation in the game engine.

2 thoughts on “Lesson 3.4: Random Dice Roller

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s