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;
}
}
}