I've been writing code for nearly 40 years now and am still not too old to learn and understand new things. Right now, my focus is a bit of OO with functional programming combined and C# with Linq is an excellent way to fool around with this.
I'm also practicing deferred execution and aggregating to make it really interesting. So I designed a dice roller. And to start, I begin by specifying various dice types:
public enum DiceType { D2 = 2, D3 = 3, D4 = 4, D6 = 6, D8 = 8, D10 = 10, D12 = 12, D20 = 20, D100 = 100 }
Very basic, actually. Each enum value just indicates the number of sides a die has. These values are very common, with the D2 (coin flip) and D6 (regular die) being the most well-known.
Next is a static class that can basically roll any dice, and will do this until infinity:
public static class Dice
{
public static IEnumerable<int> Roll()
{
var rnd = new Random();
while (true) { yield return rnd.Next(1, 7); }
}
public static IEnumerable<int> Roll(this DiceType die)
{
var rnd = new Random();
while (true) { yield return rnd.Next((int)die) + 1; }
}
public static IEnumerable<int> RollX(this IEnumerable<int> rolls, int x = 3)
{
while (true) { yield return rolls.Take(x).Sum(i => i); }
}
}
The Roll()
method will be the stereotype die with six sides. Hardcoded, as someone might change the value for D6 to 8 and cause problems. This method will always roll a six-sided die.
But Roll(this DiceType die)
is more interesting as it basically extends the DiceTyper enumeration. And it will roll that specific die into infinity so if I use DiceType.D20.Roll()
then I will get an endless list of values between 1 and 20...
The third method RollX()
is an even more interesting one. It will use the previous dice enumerator and take an X amount of values to sum them, before taking the next X amount of dice. By default, it will roll 3 times. And DiceType.D6.Roll().RollX(5)
will roll 5 dice, each resulting in a value between 1 and 6, and add them to get values between 5 and 30, and a bell curve.
Next, two clock-related aggregators that will limit the amount of time allowed to keep rolling:
public static IEnumerable<T> MaxTime<T>(this IEnumerable<T> source, long totalMs)
{
var sw = new Stopwatch();
foreach (var item in source)
{
if (!sw.IsRunning) { sw.Restart(); }
if (sw.ElapsedMilliseconds < totalMs) { yield return item; }
else { yield break; }
}
}
public static IEnumerable<T> MaxTicks<T>(this IEnumerable<T> source, long totalTicks)
{
var sw = new Stopwatch();
foreach (var item in source)
{
if (!sw.IsRunning) { sw.Restart(); }
if (sw.ElapsedTicks < totalTicks) { yield return item; }
else { yield break; }
}
}
The stopwatch class can be found in System.Diagnostics.
The principle is simple: it will do a deferred execution until the time is up, which can either be a matter of seconds or a matter of clock ticks.
As a final step, I want an aggregator to just add all the results:
public static SumTotal Total(this IEnumerable<int> data)
{
var result = new SumTotal();
foreach (var i in data) { result.Add(i); }
return result;
}
And this introduces this class:
public class SumTotal
{
public long Sum { get; set; } = 0;
public long Total { get; set; } = 0;
public long Min { get; set; } = long.MaxValue;
public long Max { get; set; } = long.MinValue;
public SumTotal Add(long value)
{
Total++;
Sum += value;
if (Max < value) { Max = value; }
if (value < Min) { Min = value; }
return this;
}
public override string ToString() => $"For {Total:#,##0} items, total of {Sum:#,##0}, average of {Sum * 1.0 / Total:#,##0.00} between {Min:#,##0} and {Max:#,##0}.";
}
So I get a total containing the number of rolls, the minimum and maximum values rolled and a total of all values and can calculate the average value rolled.
Now, to use it, all I have to do is define a rolling queue like this:
public static IEnumerable<int> D12Queue = DiceType.D12.Roll();
This generates a static variable from where I can just take an X amount of rolls. Simply using D12Queue.Take(5)
will give me 5 random values between 1 and 12. This makes it practical to use in games where you'd need to emulate a dice roll.
So, is this a practical solution? I'm just training and learning new skills so I haven't thought about that aspect...
The latter dice queue is what my main focus will be. I can use this:
var queue = DiceType.D6.Roll();
to generate a queue. And then:
var roll = queue.First();
which will give a new dice roll with every call.
var yahtzee = queue.Take(5);
to roll 5 dice and see if I rolled a Yahtzee.
var highRoll = queue.RollX(5).Take(20).Max();
Which will roll 5 dice repeatedly for 20 times and return the highest value.
So basically, I made dice rolling part of Linq queries so I don't need to write loops if I need multiple rolls. I just take the number of required rolls from the queue.