using System; using System.Collections.Generic; namespace Amphora.RandomComparisons { /// /// Checks the bounded integers and floating-point numbers drawn from a generator, which are the ones games use. /// Each is checked for being uniform over its range, and some against a naive version that is known to be wrong. /// public static class BoundedChecks { public static NamedOutcome Proportion(string name, long hits, long draws, double probability, bool expectsFailure = false) { double z = (hits - draws * probability) / Math.Sqrt(draws * probability * (1 - probability)); string detail = (100.0 * hits / draws).ToString("0.000") + "% of " + draws + " draws were in the lower part, against " + (100 * probability).ToString("0.000") + "%"; return new NamedOutcome(name, new Outcome(Stats.NormalTwoSided(z), detail), expectsFailure); } public static NamedOutcome Uniform(string name, int[] counts, long draws) { double expected = (double)draws / counts.Length; double statistic = 0; foreach (int count in counts) { statistic += (count - expected) * (count - expected) / expected; } return new NamedOutcome(name, new Outcome(Stats.ChiSquareTwoSided(statistic, counts.Length - 1), counts.Length + " equal parts, chi-square " + statistic.ToString("0.0"))); } private static NamedOutcome StaysInRange(string name, long outside, double minimum, double maximumExclusive) { string detail = outside + " draws outside [" + minimum + ", " + maximumExclusive + ")"; return new NamedOutcome(name, new Outcome(outside == 0 ? 1 : 0, detail)); } public static List Run(GeneratorType start, Scale scale) where GeneratorType : struct, IRandomGenerator { GeneratorType generator = start; long draws = scale.Count(1L << 24, minimum: 1L << 20); var returnMe = new List(); // 3 * 2^30 leaves the draws below 2^30 twice as likely as the rest if the reduction is a plain remainder. const uint thirdBound = 3U << 30; long hits = 0; for (long i = 0; i < draws; i++) { if (generator.NextUInt32(thirdBound) < (1U << 30)) { hits++; } } returnMe.Add(Proportion("32-bit bounded draw below 3*2^30", hits, draws, 1.0 / 3)); hits = 0; for (long i = 0; i < draws; i++) { if (generator.NextUInt32() % thirdBound < (1U << 30)) { hits++; } } returnMe.Add(Proportion("a plain remainder instead", hits, draws, 1.0 / 3, expectsFailure: true)); const ulong wideThirdBound = 3UL << 62; hits = 0; for (long i = 0; i < draws; i++) { if (generator.NextUInt64(wideThirdBound) < (1UL << 62)) { hits++; } } returnMe.Add(Proportion("64-bit bounded draw below 3*2^62", hits, draws, 1.0 / 3)); hits = 0; for (long i = 0; i < draws; i++) { if (generator.NextUInt64() % wideThirdBound < (1UL << 62)) { hits++; } } returnMe.Add(Proportion("a plain remainder instead", hits, draws, 1.0 / 3, expectsFailure: true)); long smallDraws = scale.Count(1L << 22, minimum: 1L << 18); var sevens = new int[7]; var thousands = new int[1000]; var longThousands = new int[1000]; for (long i = 0; i < smallDraws; i++) { sevens[generator.NextInt(7)]++; thousands[generator.NextInt(1000)]++; longThousands[(int)(generator.NextLong(1000000000000L) / 1000000000L)]++; } returnMe.Add(Uniform("NextInt(7)", sevens, smallDraws)); returnMe.Add(Uniform("NextInt(1000)", thousands, smallDraws)); returnMe.Add(Uniform("NextLong(10^12), by billions", longThousands, smallDraws)); const int cells = 1 << 14; var doubles = new int[cells]; var floats = new int[cells]; for (long i = 0; i < smallDraws; i++) { doubles[(int)(generator.NextDouble() * cells)]++; floats[(int)(generator.NextFloat() * cells)]++; } returnMe.Add(Uniform("NextDouble()", doubles, smallDraws)); returnMe.Add(Uniform("NextFloat()", floats, smallDraws)); const double minimum = -3.5; const double maximum = 7.25; var doubleRange = new int[1024]; var floatRange = new int[1024]; long doublesOutside = 0; long floatsOutside = 0; for (long i = 0; i < smallDraws; i++) { double value = generator.NextDouble(minimum, maximum); if (value < minimum || value >= maximum) { doublesOutside++; } else { doubleRange[(int)((value - minimum) / (maximum - minimum) * 1024)]++; } float floatValue = generator.NextFloat((float)minimum, (float)maximum); if (floatValue < minimum || floatValue >= maximum) { floatsOutside++; } else { floatRange[(int)((floatValue - minimum) / (maximum - minimum) * 1024)]++; } } returnMe.Add(StaysInRange("NextDouble(-3.5, 7.25) stays in its range", doublesOutside, minimum, maximum)); returnMe.Add(Uniform("NextDouble(-3.5, 7.25)", doubleRange, smallDraws - doublesOutside)); returnMe.Add(StaysInRange("NextFloat(-3.5, 7.25) stays in its range", floatsOutside, minimum, maximum)); returnMe.Add(Uniform("NextFloat(-3.5, 7.25)", floatRange, smallDraws - floatsOutside)); returnMe.AddRange(RangeChecks.Run(generator, scale)); return returnMe; } } }