package net.sf.colossus.game; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import net.sf.colossus.game.Dice; import junit.framework.TestCase; /** * JUnit test for dice. * * @author David Ripton */ public class DiceTest extends TestCase { private static final Logger LOGGER = Logger.getLogger(DiceTest.class .getName()); private final int trials = 5000; double epsilon = 0.000001; public DiceTest(String name) { super(name); } /* Commented out because it's slow. public void testDevRandom() { System.out.println("---testDevRandom()---"); int [] rolls = new int[trials]; Dice.init("/dev/random"); for (int i = 0; i < trials; i++) { rolls[i] = Dice.rollDie(); } runAllTests(rolls, true); } */ // Comented out because not everyone has a /tmp/random :-) /* public void testTmpRandom() { System.out.println("---testTmpRandom()---"); int [] rolls = new int[trials]; Dice.init("/tmp/random"); for (int i = 0; i < trials; i++) { rolls[i] = Dice.rollDie(); } run4Tests(rolls, true); } */ public void testDevUrandom() { LOGGER.log(Level.FINEST, "---testDevUrandom()---"); int[] rolls = new int[trials]; Dice.init("/dev/urandom"); for (int i = 0; i < trials; i++) { rolls[i] = Dice.rollDie(); } runAllTests(rolls, true); } public void testPRNG() { LOGGER.log(Level.FINEST, "---testPRNG()---"); int[] rolls = new int[trials]; Dice.init("PRNG"); for (int i = 0; i < trials; i++) { rolls[i] = Dice.rollDie(); } runAllTests(rolls, true); } public void testNonRandomDice() { LOGGER.log(Level.FINEST, "---testNonRandomDice()--- [some should fail]"); int[] rolls = new int[trials]; for (int i = 0; i < trials; i++) { rolls[i] = Dice.rollDieNonRandom(); } runAllTests(rolls, false); } void runAllTests(int[] rolls, boolean random) { chiSquareTest(rolls, random); mTest(rolls, random); signTest(rolls, random); mannKendallTest(rolls, random); runsTest(rolls, random); } // Most tests at http://www.stat.unc.edu/faculty/rs/s102/lec17a.pdf // Runs test http://www2.sunysuffolk.edu/wrightj/MA24/Misc/RunsTest.pdf // XXX Need to use chi-square distribution not normal distribution. void chiSquareTest(int[] rolls, boolean random) { // We know our non-random dice will ace this test, so use them // to find expected mean and variance. int divisibleBy6 = 6 * trials / 6; int[] fixedRolls = new int[divisibleBy6]; for (int i = 0; i < fixedRolls.length; i++) { fixedRolls[i] = Dice.rollDieNonRandom(); } final double meanDieRoll = 3.5; double expVariance = findExpectedVariance(fixedRolls, meanDieRoll); double expMean = findChiSquare(fixedRolls, meanDieRoll) / fixedRolls.length; double chisquare = findChiSquare(rolls, meanDieRoll) / rolls.length; LOGGER.log(Level.FINEST, "chi-square test: chi-square=" + chisquare + " mean=" + expMean + " var=" + expVariance); failIfAbnormal(chisquare, expMean, expVariance, random); } /** Recode each sample as 0 if <= sample median, 1 if > sample median M is number of runs of consecutive 0s and 1s. r is number of 0s. null hypothesis, mean and variance of M in n observations are about meanM = 2*r*(n-r)/n + 1 varianceM = 2*r*(n-r)*(2*r*(n-r)-n)/(n*n*(n-1)) for large samples Zm = (M - meanM) / standardDevM is standard normal prob (M <= val) = Pr((M-meanM)/sdM = Pr(Z) */ void mTest(int[] rolls, boolean random) { double median = findMedian(rolls); int[] ms = convertToBinary(rolls, median); int r = countZeros(ms); int M = countRuns(ms); int n = trials; double meanM = (2.0 * r * (n - r) / n) + 1.; double varianceM = ((2.0 * r) * (n - r) / n / n * ((2. * r) * (n - r) - n)) / (n - 1.); LOGGER.log(Level.FINEST, "M test: r=" + r + " M=" + M + " mean=" + meanM + " var=" + varianceM); failIfAbnormal(M, meanM, varianceM, random); } /** P is number of positive signs among x2-x1, x3-x2, etc. (not zeros) If m non-zero values of xi - x(i-1), meanP is m/2, varianceP is m/12 */ void signTest(int[] rolls, boolean random) { double P = countPositiveDiffs(rolls); double m = countNonZeroDiffs(rolls); double meanP = m / 2.; double varianceP = m / 12.; LOGGER.log(Level.FINEST, "Sign test: P=" + P + " m=" + m + " mean=" + meanP + " var=" + varianceP); failIfAbnormal(P, meanP, varianceP, random); } void runsTest(int[] rolls, boolean random) { int[] trimmed = trimZeroRuns(rolls); int m = trimmed.length; int pos = countPositiveDiffs(trimmed); int neg = m - pos; double R = 0. + pos; double meanR = 1. + (2 * pos * neg) / (pos + neg); double varianceR = ((2.0 * pos * neg) * (2 * pos * neg - pos - neg)) / ((pos + neg) * (pos + neg) * (pos + neg - 1)); LOGGER.log(Level.FINEST, "Runs test: R=" + R + " m=" + m + " mean=" + meanR + " var=" + varianceR); failIfAbnormal(R, meanR, varianceR, random); } void mannKendallTest(int[] rolls, boolean random) { int S = 0; int n = rolls.length; for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { int val = sign(rolls[i] - rolls[j]); S += val; } } double meanS = 0.; double varianceS = (n / 18.) * (n - 1.) * (2. * n + 5.); LOGGER.log(Level.FINEST, "Mann-Kendall test: S=" + S + " mean=" + meanS + " var=" + varianceS); failIfAbnormal(S, meanS, varianceS, random); } double findMean(int[] rolls) { double sum = 0.0; for (int roll : rolls) { sum += roll; } return sum / rolls.length; } double findMedian(int[] rolls) { int[] copy = rolls.clone(); Arrays.sort(copy); double midpoint = (rolls.length - 1) / 2.0; if (Math.abs(midpoint - Math.round(midpoint)) < epsilon) { return copy[(int)Math.round(midpoint)]; } else { return (copy[(int)Math.round(midpoint - 0.5)] + copy[(int)Math .round(midpoint + 0.5)]) / 2.0; } } int[] convertToBinary(int[] rolls, double median) { int[] ms = new int[rolls.length]; for (int i = 0; i < rolls.length; i++) { if (rolls[i] <= median) { ms[i] = 0; } else { ms[i] = 1; } } return ms; } int countZeros(int[] rolls) { int count = 0; for (int roll : rolls) { if (roll == 0) { count++; } } return count; } int countRuns(int[] rolls) { int count = 0; for (int i = 0; i < rolls.length; i++) { if (i == 0 || rolls[i] != rolls[i - 1]) { count++; } } return count; } int countPositiveDiffs(int[] rolls) { int count = 0; for (int i = 0; i < rolls.length - 1; i++) { if (rolls[i + 1] > rolls[i]) { count++; } } return count; } int countNonZeroDiffs(int[] rolls) { int count = 0; for (int i = 1; i < rolls.length; i++) { if (rolls[i] != rolls[i - 1]) { count++; } } return count; } /** Return the list with runs of identical rolls reduced to just one */ int[] trimZeroRuns(int[] rolls) { List<Integer> li = new ArrayList<Integer>(); int lastroll = -1; for (int roll : rolls) { if (roll != lastroll) { lastroll = roll; li.add(Integer.valueOf(lastroll)); } } int[] results = new int[li.size()]; for (int i = 0; i < results.length; i++) { results[i] = li.get(i).intValue(); } return results; } /** Return the standard normal probability p(z) */ double normalDistribution(double z) { return 1 / Math.sqrt(2 * Math.PI) * Math.exp(-z * z / 2); } /** Return 1 if positive, 0 if zero, -1 if negative. */ int sign(int num) { if (num > 0) { return 1; } else if (num < 0) { return -1; } return 0; } double findChiSquare(int[] rolls, double mean) { double sum = 0.; for (int roll : rolls) { double diff = roll - mean; sum += diff * diff; } return sum / mean; } double findExpectedVariance(int[] rolls, double mean) { double sum = 0.; int n = rolls.length; for (int i = 0; i < n; i++) { int x = rolls[i]; sum += Math.pow(x - mean, 2); } return sum / n; } /** Fail if an expected random result is outside the normal range. */ void failIfAbnormal(double val, double mean, double variance, boolean random) { double sd; double z; // Avoid division by zero when we hit spot-on. if (Math.abs(variance) < epsilon) { sd = 0; z = 0; } else { sd = Math.sqrt(Math.abs(variance)); z = (val - mean) / sd; } LOGGER.log(Level.FINEST, "sd=" + sd + " z=" + z); if (Math.abs(z) > 3) { if (random) { LOGGER.log(Level.FINEST, " (FAILURE)"); fail("Random result is outside expected normal range."); } else { LOGGER.log(Level.FINEST, " (expected failure)"); } } else { LOGGER.log(Level.FINEST, " (success)"); } } }