I've previously came up with the question "Rolling dice in a method chain" and was interested if this could be improved. It could, so now I have this class:
public class DiceQueue : IEnumerable<int>
{
public int Sides { get; }
internal Random Rnd = new Random();
public DiceQueue(int sides) { Sides = sides; }
public int Roll() => Rnd.Next(Sides) + 1;
public IEnumerator<int> GetEnumerator() { while (true) { yield return Roll(); } }
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
Very simple: just create a queue by calling the constructor with a number of sides and then it can be used as an infinite enumerator. But generally, you just want to take an X amount of values from the queue at once, or even just roll a single die. But this class isn't that interesting, as I want to do statistics with random rolls!
So, hence this class:
public class Statistics : IEnumerable<Point<long>>
{
public string Name { get; private set; }
public long Sum { get; private set; } = 0;
public long Total { get; private set; } = 0;
public long Min { get; private set; } = long.MaxValue;
public long Max { get; private set; } = long.MinValue;
public double Average => 1.0 * Sum / Total;
public double TotalOf(int value) => Totals.ContainsKey(value) ? 100.0 * Totals[value] / Total : 0.0;
protected Dictionary<long, long> Totals { get; set; } = new Dictionary<long, long>();
public event EventHandler<StatisticsEventArgs> Added;
protected virtual void OnAdded(StatisticsEventArgs e) { Added?.Invoke(this, e); }
public Statistics() : this("No name") { }
public Statistics(string name) { Name = name; }
public Statistics Add(long value)
{
if (!Totals.ContainsKey(value)) { Totals.Add(value, 0); }
Totals[value] += 1;
Total++;
Sum += value;
if (Max < value) { Max = value; }
if (value < Min) { Min = value; }
OnAdded(new StatisticsEventArgs(this, value));
return this;
}
public IEnumerable<string> Report()
{
yield return $"Report for: {Name}";
yield return $"The total sum is {Sum} for {Total} values for an average value of {Average:0.00}.";
yield return $"The values are all between {Min} and {Max}.";
yield return "List of values";
foreach (var point in this) { yield return $"* Value {point.X} occurred {point.Y} times: {100.0 * point.Y / Total:00.0}%."; }
yield return new string('-', 40);
}
public override string ToString() => string.Join(Environment.NewLine, Report());
public IEnumerator<Point<long>> GetEnumerator()
{
foreach (var total in Totals.OrderBy(pair => pair.Key)) { yield return new Point<long> { X = total.Key, Y = total.Value }; }
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
It has a name and you just add values to it and it will keep various statistics about it. The Report() method will generate a textual report of the summary but it's mainly used for the ToString() method. Interesting way to make multiline reports, btw.
It also has an event which gets triggered whenever a new value is added, allowing the system to display a running total.
I have to show a few more types, though. First:
public class Point<T>
{
public T X { get; internal set; }
public T Y { get; internal set; }
}
Not really rocket science. And while C# does have a Point datatype somewhere, I wanted one that's more generic...
public class StatisticsEventArgs : EventArgs
{
public Statistics Stats { get; }
public long LastValue { get; }
public StatisticsEventArgs(Statistics stats, long lastValue)
{
Stats = stats;
LastValue = lastValue;
}
}
Yeah, well... The event requires an argument. This one should be it. Trying to follow the standard for events here.
But I want to use the statistics in a chainable way and I want to do statistics on rolls of multiple dice, so I also have these extension methods:
public static class Toolkit
{
public static IEnumerable<T> Do<T>(this IEnumerable<T> data, Action<T> action)
{
foreach (var item in data)
{
action(item);
yield return item;
}
}
public static IEnumerable<List<T>> TakeGroup<T>(this IEnumerable<T> data, int count)
{
while (true) { yield return data.Take(count).ToList(); }
}
}
The Do() method just does some action with a value from an enumerator before passing the value on to the next method. Maybe not elegant, but I have no other options here, as far as I know.
The TakeGroup() method will grab an X amount of values and return them as a list, and keeps doing this until infinity. Too bad that this might cause trouble if the base enumeration is limited so I need to work out a solution for that. A Partitioner, perhaps? Well, still have to work that out but this works for my current purpose...
Then something to test the whole thing.
public static class StatisticsTest
{
public static void Execute(int count)
{
Console.WriteLine($"We've rolled for a total of {D6X3RollQueue.Take(count).Count()} times.");
Console.WriteLine(D6Stat);
Console.WriteLine(D6X3MinStat);
Console.WriteLine(D6X3MaxStat);
Console.WriteLine(D6X3MinMaxStat);
Console.WriteLine(D6X3SumStat);
}
private static readonly DiceQueue D6 = new DiceQueue(6);
private static readonly Statistics D6Stat = new Statistics("Rolling d6");
private static readonly Statistics D6X3MinStat = new Statistics("Minimum of 3D6 (1 to 6)");
private static readonly Statistics D6X3MaxStat = new Statistics("Maximum of 3D6 (1 to 6)");
private static readonly Statistics D6X3MinMaxStat = new Statistics("Sum(Min+Max) of 3D6 (2 to 12)");
private static readonly Statistics D6X3SumStat = new Statistics("Sum of 3D6 (3 to 18)");
private static readonly IEnumerable<int> D6RollQueue = D6.Do(d => D6Stat.Add(d));
private static readonly IEnumerable<int> D6X3RollQueue = D6RollQueue.TakeGroup(3)
.Do(d => D6X3MinStat.Add(d.Min()))
.Do(d => D6X3MaxStat.Add(d.Max()))
.Do(d => D6X3MinMaxStat.Add(d.Min() + d.Max()))
.Select(d => d.Sum())
.Do(d => D6X3SumStat.Add(d));
}
This shows why I want a dice queue. The queue allows me to collect various statistics for all the rolls made while I can roll as many times as I like. By declaring a queue like D6RollQueue
I can take a single roll by using D6RollQueue.First()
or an X amount of rolls by using `D6RollQueue.Take(X)' and then do something with the result, while the queue keeps track of all statistics. So when I create some dice game, I can just roll as often as I like while having various statistics about the rolls.
And the TakeGroup() method will allow me to maintain statistics about groups of rolls like rolling three dice and keep statistics about the minimum and maximum values.
So, go ahead and shoot at it. Can it be improved?
(It's not that important that the Random generator might be predictable.)