Finding the sum and adding a probability scale

Viewed 349

I am working on an assignment where I need to create a fully functioning Yahtzee program. I am now done with the initial task, and have a fully functioning Yahtzee application, so I can roll the dice and I can hold values etc. But the problem I am facing right now is that I need to add 2 special dies to the mix, one of the dies need to have a higher probability of outputting low numbers while the other die need to have a higher probability of outputting higher numbers, also I need to provide a way for the user to adjust the probability on these dies within the console window.

So basically I was looking around on the internet and it seemed like, if I hardcoded it with an array, where I basically put more low numbers than high numbers, it does work, but that is very messy, and also doesn't fix the issue and also makes the task of allowing the user to adjust the probability very silly.

Long time since I've used the site, apologize if this is a bit sloppy.

class Dice
{
    public static readonly Random rng = new Random();

        public int NumberOfDice { get; private set; }

        public Dice()
        {
            NumberOfDice = rng.Next(1, 7);
        }

        public void Reroll()
        {
            NumberOfDice = rng.Next(1, 7);
        }
    }

    public bool FirstRound { get; private set; } = true;

    public readonly Dice[] Dices =
    {
        new Dice(),
        new Dice(),
        new Dice(),
        new Dice(),
        new Dice(),

    public void KeepDices(int[] toReroll)
    {
        for (int i = 0; i < Dices.Length; i++)
        {
            bool numFound = false;

            foreach (int roll in toReroll)
            {
                if (Dices[i].NumberOfDice == roll)
                {
                    numFound = true;
                }
            }

            if (numFound)
            {
                continue;
            }
            else
            {
                Dices[i].Reroll();
            }
        }

        ShowDices();
    }

    public void ShowDices()
    {
        Console.WriteLine();

        foreach (Dice dice in Dices)
        {
            Console.Write("[{0}] ", dice.NumberOfDice);
            FirstRound = false;
        }

        Console.WriteLine();
        Console.WriteLine();
    }

    public void RerollAll()
    {
        foreach (Dice dice in Dices)
        {
            dice.Reroll();
        }

        ShowDices();
    }
}

As I said the program runs fine, it works, but just need to somehow add a way to tie two of the dices to probability.

4 Answers

I have recently written a 40-part blog series on how to more elegantly build randomness into C# programs. Your problem is covered in part 9:

https://ericlippert.com/2019/02/28/fixing-random-part-9/

In short: create a non uniform weighted distribution object to represent the distribution. You can implement it efficiently with the alias method. You then sample from that distribution to roll your die.

You can then create a game distribution by summing the results of various die distributions, and sample from it.

I suggest that you read the entire series; you will learn a lot from it. It begins here:

https://ericlippert.com/2019/01/31/fixing-random-part-1/

You can find the source code for the implementation of the alias method here:

https://github.com/ericlippert/probability/tree/episode09

You can create an array of probabilities for each face of the dice.

var loadedDice = new double[] { 0.1, 0.12, 0.18, 0.2, 0.21, 0.19 };

Now you generate a random number between 0.0 and 1.0 and calculate a running sum of these probabilities, until the sum is greater or equal to the random number.

// Play the dice
double p = rng.NextDouble() * loadedDice.Sum();

double sum = 0.0;
for (int i = 0; i < 6; i++) {
    sum += loadedDice[i];
    if (sum >= p) {
        Console.Write($"The number is {i + 1}");
        break;
    }
}

We add one, because the indexes start at 0.


Example:

The xs in this image represent the running sum. The distances between them correspond to the probabilities in the array. If say we get a random number of 0.27239827 (p in the image), then the running sum becomes greater than p for index = 2. We rolled number 3.

     x     x p      x         x          x        x
+----+----+----+----+----+----+----+----+----+----+
0   0.1  0.2  0.3  0.4  0.5  0.6  0.7  0.8  0.9  1.0

One way to do this is to add a property called ChanceOfRollingSmallNumber into your Dice class:

public int ChanceOfRollingSmallNumber { get; set; } = 50;

When rolling the dice, generate a random number between 0 and 99. If the number is less than the ChanceOfRollingSmallNumber, then generate a small random number, otherwise generate a large random number:

if (rng.Next(100) < ChanceOfRollingSmallNumber) {
    NumberOfDice = rng.Next(1, 4);
} else {
    NumberOfDice = rng.Next(4, 7);
}

For the dice that has a high probability of rolling a small number, just set ChanceOfRollingSmallNumber to something big. For the dice that has a high probably of rolling a big number, just set it to something small. For the fair dices, just set it to 50.

If you want even finer tuning, you could make ChanceOfRollingSmallNumber range from 0 to 1000, or something even bigger than that.

I think what you're looking for is a weighted random number generator.

So I think you should be able to adopt this solution for your need.

First, a weight class to define your weights. This would be what you'd use for your sides of the dice.

public class WeightUnit
{
    public string Name = string.Empty;
    public int Weight = 0;

    public WeightUnit(string n, int w)
    {
        this.Name = n;
        this.Weight = w;
    }
}

Then this method will return a weighted random number:

public static WeightUnit GetRandom(List<WeightUnit> weights)
{
    var totalWeight = weights.Sum(x => x.Weight);
    int randomNumber = _rnd.Next(0, totalWeight);

    WeightUnit selected = null;
    foreach (WeightUnit unit in weights)
    {
        if (randomNumber < unit.Weight)
        {
            selected = unit;
            break;
        }

        randomNumber = randomNumber - unit.Weight;
    }

    return selected;
}

And here's a main program to verify:

private static Random _rnd = new Random();

static void Main()
{
    List<WeightUnit> weights = new List<WeightUnit>
    {
        new WeightUnit("1", 20),
        new WeightUnit("2", 20),
        new WeightUnit("3", 20),
        new WeightUnit("4", 10),
        new WeightUnit("5", 10),
        new WeightUnit("6", 10)
    };

    Dictionary<string, int> result = new Dictionary<string, int>();

    WeightUnit selected = null;

    for (int i = 0; i < 1000; i++)
    {
        selected = GetRandom(weights);
        if (selected != null)
        {
            if (result.ContainsKey(selected.Name))
            {
                result[selected.Name] = result[selected.Name] + 1;
            }
            else
            {
                result.Add(selected.Name, 1);
            }
        }
    }


    Console.WriteLine("1\t\t" + result["1"]);
    Console.WriteLine("2\t\t" + result["2"]);
    Console.WriteLine("3\t\t" + result["3"]);
    Console.WriteLine("4\t\t" + result["4"]);
    Console.WriteLine("5\t\t" + result["5"]);
    Console.WriteLine("6\t\t" + result["6"]);
    Console.ReadLine();
}

As you can see, I've assigned twice the weightage for numbers 1,2, and 3, and over 1,000 iterations they're selected twice as many times compared to 4, 5, and 5. So when you're making your list you can assign the weightage you want.

enter image description here

Related