From 12a02b84038b4b35deea6a41978cd9192de32a53 Mon Sep 17 00:00:00 2001 From: Jeff Moser Date: Sat, 28 Aug 2010 22:05:41 -0400 Subject: [PATCH] First TwoPlayerTrueSkillCalculator unit test passed --- PHPSkills/Elo/EloRating.php | 19 ++ PHPSkills/Elo/FideEloCalculator.php | 41 +++ PHPSkills/Elo/FideKFactor.php | 30 +++ PHPSkills/Elo/GaussianEloCalculator.php | 26 ++ PHPSkills/Elo/KFactor.php | 22 ++ PHPSkills/Elo/TwoPlayerEloCalculator.php | 105 ++++++++ PHPSkills/GameInfo.php | 70 +++++ PHPSkills/Guard.php | 35 +++ PHPSkills/ISupportPartialPlay.php | 16 ++ PHPSkills/ISupportPartialUpdate.php | 11 + PHPSkills/Numerics/BasicMath.php | 17 ++ PHPSkills/Numerics/GaussianDistribution.php | 247 ++++++++++++++++++ PHPSkills/Numerics/Range.php | 62 +++++ PHPSkills/PairwiseComparison.php | 27 ++ PHPSkills/Player.php | 80 ++++++ PHPSkills/PlayersRange.php | 21 ++ PHPSkills/RankSorter.php | 23 ++ PHPSkills/Rating.php | 79 ++++++ PHPSkills/RatingContainer.php | 46 ++++ PHPSkills/SkillCalculator.php | 81 ++++++ PHPSkills/Team.php | 24 ++ PHPSkills/Teams.php | 18 ++ PHPSkills/TeamsRange.php | 21 ++ PHPSkills/TrueSkill/DrawMargin.php | 26 ++ .../TruncatedGaussianCorrectionFunctions.php | 135 ++++++++++ .../TwoPlayerTrueSkillCalculator.php | 173 ++++++++++++ UnitTests/Elo/EloAssert.php | 46 ++++ UnitTests/Elo/FideEloCalculatorTest.php | 39 +++ UnitTests/Numerics/BasicMathTest.php | 15 ++ .../Numerics/GaussianDistributionTest.php | 106 ++++++++ UnitTests/RankSorterTest.php | 33 +++ UnitTests/TrueSkill/DrawMarginTest.php | 36 +++ .../TrueSkill/TrueSkillCalculatorTests.php | 76 ++++++ .../TwoPlayerTrueSkillCalculatorTest.php | 26 ++ UnitTests/runner_example.php | 33 +++ nbproject/private/private.properties | 5 + nbproject/private/private.xml | 4 + nbproject/project.properties | 7 + nbproject/project.xml | 9 + test.php | 8 + web.config | 11 + 41 files changed, 1909 insertions(+) create mode 100644 PHPSkills/Elo/EloRating.php create mode 100644 PHPSkills/Elo/FideEloCalculator.php create mode 100644 PHPSkills/Elo/FideKFactor.php create mode 100644 PHPSkills/Elo/GaussianEloCalculator.php create mode 100644 PHPSkills/Elo/KFactor.php create mode 100644 PHPSkills/Elo/TwoPlayerEloCalculator.php create mode 100644 PHPSkills/GameInfo.php create mode 100644 PHPSkills/Guard.php create mode 100644 PHPSkills/ISupportPartialPlay.php create mode 100644 PHPSkills/ISupportPartialUpdate.php create mode 100644 PHPSkills/Numerics/BasicMath.php create mode 100644 PHPSkills/Numerics/GaussianDistribution.php create mode 100644 PHPSkills/Numerics/Range.php create mode 100644 PHPSkills/PairwiseComparison.php create mode 100644 PHPSkills/Player.php create mode 100644 PHPSkills/PlayersRange.php create mode 100644 PHPSkills/RankSorter.php create mode 100644 PHPSkills/Rating.php create mode 100644 PHPSkills/RatingContainer.php create mode 100644 PHPSkills/SkillCalculator.php create mode 100644 PHPSkills/Team.php create mode 100644 PHPSkills/Teams.php create mode 100644 PHPSkills/TeamsRange.php create mode 100644 PHPSkills/TrueSkill/DrawMargin.php create mode 100644 PHPSkills/TrueSkill/TruncatedGaussianCorrectionFunctions.php create mode 100644 PHPSkills/TrueSkill/TwoPlayerTrueSkillCalculator.php create mode 100644 UnitTests/Elo/EloAssert.php create mode 100644 UnitTests/Elo/FideEloCalculatorTest.php create mode 100644 UnitTests/Numerics/BasicMathTest.php create mode 100644 UnitTests/Numerics/GaussianDistributionTest.php create mode 100644 UnitTests/RankSorterTest.php create mode 100644 UnitTests/TrueSkill/DrawMarginTest.php create mode 100644 UnitTests/TrueSkill/TrueSkillCalculatorTests.php create mode 100644 UnitTests/TrueSkill/TwoPlayerTrueSkillCalculatorTest.php create mode 100644 UnitTests/runner_example.php create mode 100644 nbproject/private/private.properties create mode 100644 nbproject/private/private.xml create mode 100644 nbproject/project.properties create mode 100644 nbproject/project.xml create mode 100644 test.php create mode 100644 web.config diff --git a/PHPSkills/Elo/EloRating.php b/PHPSkills/Elo/EloRating.php new file mode 100644 index 0000000..ec9c027 --- /dev/null +++ b/PHPSkills/Elo/EloRating.php @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/PHPSkills/Elo/FideEloCalculator.php b/PHPSkills/Elo/FideEloCalculator.php new file mode 100644 index 0000000..da3cbdf --- /dev/null +++ b/PHPSkills/Elo/FideEloCalculator.php @@ -0,0 +1,41 @@ +getBeta())) + ); + } +} + +?> \ No newline at end of file diff --git a/PHPSkills/Elo/FideKFactor.php b/PHPSkills/Elo/FideKFactor.php new file mode 100644 index 0000000..405a198 --- /dev/null +++ b/PHPSkills/Elo/FideKFactor.php @@ -0,0 +1,30 @@ + \ No newline at end of file diff --git a/PHPSkills/Elo/GaussianEloCalculator.php b/PHPSkills/Elo/GaussianEloCalculator.php new file mode 100644 index 0000000..ff16154 --- /dev/null +++ b/PHPSkills/Elo/GaussianEloCalculator.php @@ -0,0 +1,26 @@ +getBeta())); + } +} + +?> \ No newline at end of file diff --git a/PHPSkills/Elo/KFactor.php b/PHPSkills/Elo/KFactor.php new file mode 100644 index 0000000..995d052 --- /dev/null +++ b/PHPSkills/Elo/KFactor.php @@ -0,0 +1,22 @@ +_value = $exactKFactor; + } + + public function getValueForRating($rating) + { + return $this->_value; + } +} + +?> \ No newline at end of file diff --git a/PHPSkills/Elo/TwoPlayerEloCalculator.php b/PHPSkills/Elo/TwoPlayerEloCalculator.php new file mode 100644 index 0000000..b863ae1 --- /dev/null +++ b/PHPSkills/Elo/TwoPlayerEloCalculator.php @@ -0,0 +1,105 @@ +_kFactor = $kFactor; + } + + public function calculateNewRatings($gameInfo, + array $teamsOfPlayerToRatings, + array $teamRanks) + { + $this->validateTeamCountAndPlayersCountPerTeam($teamsOfPlayerToRatings); + RankSorter::sort($teamsOfPlayerToRatings, $teamRanks); + + $result = array(); + $isDraw = ($teamRanks[0] === $teamRanks[1]); + + $team1 = $teamsOfPlayerToRatings[0]; + $team2 = $teamsOfPlayerToRatings[1]; + + $player1 = each($team1); + $player2 = each($team2); + + $player1Rating = $player1["value"]->getMean(); + $player2Rating = $player2["value"]->getMean(); + + $result[$player1["key"]] = $this->calculateNewRating($gameInfo, $player1Rating, $player2Rating, $isDraw ? PairwiseComparison::DRAW : PairwiseComparison::WIN); + $result[$player2["key"]] = $this->calculateNewRating($gameInfo, $player2Rating, $player1Rating, $isDraw ? PairwiseComparison::DRAW : PairwiseComparison::LOSE); + + return $result; + } + + protected function calculateNewRating($gameInfo, $selfRating, $opponentRating, $selfToOpponentComparison) + { + $expectedProbability = $this->getPlayerWinProbability($gameInfo, $selfRating, $opponentRating); + $actualProbability = $this->getScoreFromComparison($selfToOpponentComparison); + $k = $this->_kFactor->getValueForRating($selfRating); + $ratingChange = $k * ($actualProbability - $expectedProbability); + $newRating = $selfRating + $ratingChange; + + return new EloRating($newRating); + } + + private static function getScoreFromComparison($comparison) + { + switch ($comparison) + { + case PairwiseComparison::WIN: + return 1; + case PairwiseComparison::DRAW: + return 0.5; + case PairwiseComparison::LOSE: + return 0; + default: + throw new Exception("Unexpected comparison"); + } + } + + public abstract function getPlayerWinProbability($gameInfo, $playerRating, $opponentRating); + + public function calculateMatchQuality($gameInfo, array $teamsOfPlayerToRatings) + { + validateTeamCountAndPlayersCountPerTeam($teamsOfPlayerToRatings); + $team1 = $teamsOfPlayerToRatings[0]; + $team2 = $teamsOfPlayerToRatings[1]; + + $player1 = $team1[0]; + $player2 = $team2[0]; + + $player1Rating = $player1[1]->getMean(); + $player2Rating = $player2[1]->getMean(); + + $ratingDifference = $player1Rating - $player2Rating; + + // The TrueSkill paper mentions that they used s1 - s2 (rating difference) to + // determine match quality. I convert that to a percentage as a delta from 50% + // using the cumulative density function of the specific curve being used + $deltaFrom50Percent = abs(getPlayerWinProbability($gameInfo, $player1Rating, $player2Rating) - 0.5); + return (0.5 - $deltaFrom50Percent) / 0.5; + } +} + +?> \ No newline at end of file diff --git a/PHPSkills/GameInfo.php b/PHPSkills/GameInfo.php new file mode 100644 index 0000000..47b6b43 --- /dev/null +++ b/PHPSkills/GameInfo.php @@ -0,0 +1,70 @@ +_initialMean = $initialMean; + $this->_initialStandardDeviation = $initialStandardDeviation; + $this->_beta = $beta; + $this->_dynamicsFactor = $dynamicsFactor; + $this->_drawProbability = $drawProbability; + } + + + public function getInitialMean() + { + return $this->_initialMean; + } + + public function getInitialStandardDeviation() + { + return $this->_initialStandardDeviation; + } + + public function getBeta() + { + return $this->_beta; + } + + public function getDynamicsFactor() + { + return $this->_dynamicsFactor; + } + + public function getDrawProbability() + { + return $this->_drawProbability; + } + + public function getDefaultRating() + { + return new Rating($this->_initialMean, $this->_initialStandardDeviation); + } +} + +?> \ No newline at end of file diff --git a/PHPSkills/Guard.php b/PHPSkills/Guard.php new file mode 100644 index 0000000..7563b1e --- /dev/null +++ b/PHPSkills/Guard.php @@ -0,0 +1,35 @@ + +/// Verifies argument contracts. +/// +/// These are used until .NET 4.0 ships with Contracts. For more information, +/// see http://www.moserware.com/2008/01/borrowing-ideas-from-3-interesting.html +class Guard +{ + public static function argumentNotNull($value, $parameterName) + { + if ($value == null) + { + throw new Exception($parameterName . " can not be null"); + } + } + + public static function argumentIsValidIndex($index, $count, $parameterName) + { + if (($index < 0) || ($index >= $count)) + { + throw new Exception($parameterName . " is an invalid index"); + } + } + + public static function argumentInRangeInclusive($value, $min, $max, $parameterName) + { + if (($value < $min) || ($value > $max)) + { + throw new Exception($parameterName . " is not in the valid range [" . $min . ", " . $max . "]"); + } + } +} +?> diff --git a/PHPSkills/ISupportPartialPlay.php b/PHPSkills/ISupportPartialPlay.php new file mode 100644 index 0000000..31fa0e3 --- /dev/null +++ b/PHPSkills/ISupportPartialPlay.php @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/PHPSkills/ISupportPartialUpdate.php b/PHPSkills/ISupportPartialUpdate.php new file mode 100644 index 0000000..5e0f5c9 --- /dev/null +++ b/PHPSkills/ISupportPartialUpdate.php @@ -0,0 +1,11 @@ + \ No newline at end of file diff --git a/PHPSkills/Numerics/BasicMath.php b/PHPSkills/Numerics/BasicMath.php new file mode 100644 index 0000000..b487d0e --- /dev/null +++ b/PHPSkills/Numerics/BasicMath.php @@ -0,0 +1,17 @@ + + * @copyright 2010 Jeff Moser + */ + +function square($x) +{ + return $x * $x; +} +?> \ No newline at end of file diff --git a/PHPSkills/Numerics/GaussianDistribution.php b/PHPSkills/Numerics/GaussianDistribution.php new file mode 100644 index 0000000..999c5ce --- /dev/null +++ b/PHPSkills/Numerics/GaussianDistribution.php @@ -0,0 +1,247 @@ + + * @copyright 2010 Jeff Moser + */ + +namespace Moserware\Numerics; + +require_once(dirname(__FILE__) . "/basicmath.php"); + +class GaussianDistribution +{ + private $_mean; + private $_standardDeviation; + + // Precision and PrecisionMean are used because they make multiplying and dividing simpler + // (the the accompanying math paper for more details) + private $_precision; + private $_precisionMean; + private $_variance; + + function __construct($mean = 0.0, $standardDeviation = 1.0) + { + $this->_mean = $mean; + $this->_standardDeviation = $standardDeviation; + $this->_variance = square($standardDeviation); + $this->_precision = 1.0/$this->_variance; + $this->_precisionMean = $this->_precision*$this->_mean; + } + + public function getMean() + { + return $this->_mean; + } + + public function getVariance() + { + return $this->_variance; + } + + public function getStandardDeviation() + { + return $this->_standardDeviation; + } + + public function getNormalizationConstant() + { + // Great derivation of this is at http://www.astro.psu.edu/~mce/A451_2/A451/downloads/notes0.pdf + return 1.0/(sqrt(2*M_PI)*$this->_standardDeviation); + } + + public function __clone() + { + $result = new GaussianDistribution(); + $result->_mean = $this->_mean; + $result->_standardDeviation = $this->_standardDeviation; + $result->_variance = $this->_variance; + $result->_precision = $this->_precision; + $result->_precisionMean = $this->_precisionMean; + return $result; + } + + public static function fromPrecisionMean($precisionMean, $precision) + { + $result = new GaussianDistribution(); + $result->_precision = $precision; + $result->_precisionMean = $precisionMean; + $result->_variance = 1.0/$precision; + $result->_standardDeviation = sqrt($result->_variance); + $result->_mean = $result->_precisionMean/$result->_precision; + return $result; + } + + // For details, see http://www.tina-vision.net/tina-knoppix/tina-memo/2003-003.pdf + // for multiplication, the precision mean ones are easier to write :) + public static function multiply(GaussianDistribution $left, GaussianDistribution $right) + { + return GaussianDistribution::fromPrecisionMean($left->_precisionMean + $right->_precisionMean, $left->_precision + $right->_precision); + } + + // Computes the absolute difference between two Gaussians + public static function absoluteDifference(GaussianDistribution $left, GaussianDistribution $right) + { + return max( + abs($left->_precisionMean - $right->_precisionMean), + sqrt(abs($left->_precision - $right->_precision))); + } + + // Computes the absolute difference between two Gaussians + public static function subtract(GaussianDistribution $left, GaussianDistribution $right) + { + return absoluteDifference($left, $right); + } + + public static function logProductNormalization(GaussianDistribution $left, GaussianDistribution $right) + { + if (($left->_precision == 0) || ($right->_precision == 0)) + { + return 0; + } + + $varianceSum = $left->_variance + $right->_variance; + $meanDifference = $left->_mean - $right->_mean; + + $logSqrt2Pi = log(sqrt(2*M_PI)); + return -$logSqrt2Pi - (log($varianceSum)/2.0) - (square($meanDifference)/(2.0*$varianceSum)); + } + + public static function divide(GaussianDistribution $numerator, GaussianDistribution $denominator) + { + return GaussianDistribution::fromPrecisionMean($numerator->_precisionMean - $denominator->_precisionMean, + $numerator->_precision - $denominator->_precision); + } + + public static function logRatioNormalization(GaussianDistribution $numerator, GaussianDistribution $denominator) + { + if (($numerator->_precision == 0) || ($denominator->_precision == 0)) + { + return 0; + } + + $varianceDifference = $denominator->_variance - $numerator->_variance; + $meanDifference = $numerator->_mean - $denominator->_mean; + + $logSqrt2Pi = log(sqrt(2*M_PI)); + + return log($denominator->_variance) + $logSqrt2Pi - log($varianceDifference)/2.0 + + square($meanDifference)/(2*$varianceDifference); + } + + public static function at($x, $mean = 0.0, $standardDeviation = 1.0) + { + // See http://mathworld.wolfram.com/NormalDistribution.html + // 1 -(x-mean)^2 / (2*stdDev^2) + // P(x) = ------------------- * e + // stdDev * sqrt(2*pi) + + $multiplier = 1.0/($standardDeviation*sqrt(2*M_PI)); + $expPart = exp((-1.0*square($x - $mean))/(2*square($standardDeviation))); + $result = $multiplier*$expPart; + return $result; + } + + public static function cumulativeTo($x, $mean = 0.0, $standardDeviation = 1.0) + { + $invsqrt2 = -0.707106781186547524400844362104; + $result = GaussianDistribution::errorFunctionCumulativeTo($invsqrt2*$x); + return 0.5*$result; + } + + private static function errorFunctionCumulativeTo($x) + { + // Derived from page 265 of Numerical Recipes 3rd Edition + $z = abs($x); + + $t = 2.0/(2.0 + $z); + $ty = 4*$t - 2; + + $coefficients = array( + -1.3026537197817094, + 6.4196979235649026e-1, + 1.9476473204185836e-2, + -9.561514786808631e-3, + -9.46595344482036e-4, + 3.66839497852761e-4, + 4.2523324806907e-5, + -2.0278578112534e-5, + -1.624290004647e-6, + 1.303655835580e-6, + 1.5626441722e-8, + -8.5238095915e-8, + 6.529054439e-9, + 5.059343495e-9, + -9.91364156e-10, + -2.27365122e-10, + 9.6467911e-11, + 2.394038e-12, + -6.886027e-12, + 8.94487e-13, + 3.13092e-13, + -1.12708e-13, + 3.81e-16, + 7.106e-15, + -1.523e-15, + -9.4e-17, + 1.21e-16, + -2.8e-17 ); + + $ncof = count($coefficients); + $d = 0.0; + $dd = 0.0; + + for ($j = $ncof - 1; $j > 0; $j--) + { + $tmp = $d; + $d = $ty*$d - $dd + $coefficients[$j]; + $dd = $tmp; + } + + $ans = $t*exp(-$z*$z + 0.5*($coefficients[0] + $ty*$d) - $dd); + return ($x >= 0.0) ? $ans : (2.0 - $ans); + } + + private static function inverseErrorFunctionCumulativeTo($p) + { + // From page 265 of numerical recipes + + if ($p >= 2.0) + { + return -100; + } + if ($p <= 0.0) + { + return 100; + } + + $pp = ($p < 1.0) ? $p : 2 - $p; + $t = sqrt(-2*log($pp/2.0)); // Initial guess + $x = -0.70711*((2.30753 + $t*0.27061)/(1.0 + $t*(0.99229 + $t*0.04481)) - $t); + + for ($j = 0; $j < 2; $j++) + { + $err = GaussianDistribution::errorFunctionCumulativeTo($x) - $pp; + $x += $err/(1.12837916709551257*exp(-square($x)) - $x*$err); // Halley + } + + return ($p < 1.0) ? $x : -$x; + } + + public static function inverseCumulativeTo($x, $mean = 0.0, $standardDeviation = 1.0) + { + // From numerical recipes, page 320 + return $mean - sqrt(2)*$standardDeviation*GaussianDistribution::inverseErrorFunctionCumulativeTo(2*$x); + } + + public function __toString() + { + return 'mean=' . $this->_mean . ' standardDeviation=' . $this->_standardDeviation; + } +} +?> \ No newline at end of file diff --git a/PHPSkills/Numerics/Range.php b/PHPSkills/Numerics/Range.php new file mode 100644 index 0000000..544c57c --- /dev/null +++ b/PHPSkills/Numerics/Range.php @@ -0,0 +1,62 @@ + $max) + { + throw new Exception("min > max"); + } + + $this->_min = $min; + $this->_max = $max; + } + + public function getMin() + { + return $this->_min; + } + + public function getMax() + { + return $this->_max; + } + + protected static function create($min, $max) + { + return new Range($min, $max); + } + + // REVIEW: It's probably bad form to have access statics via a derived class, but the syntax looks better :-) + + public static function inclusive($min, $max) + { + return static::create($min, $max); + } + + public static function exactly($value) + { + return static::create($value, $value); + } + + public static function atLeast($minimumValue) + { + return static::create($minimumValue, PHP_INT_MAX ); + } + + public function isInRange($value) + { + return ($this->_min <= $value) && ($value <= $this->_max); + } +} + +?> diff --git a/PHPSkills/PairwiseComparison.php b/PHPSkills/PairwiseComparison.php new file mode 100644 index 0000000..e81ec92 --- /dev/null +++ b/PHPSkills/PairwiseComparison.php @@ -0,0 +1,27 @@ + \ No newline at end of file diff --git a/PHPSkills/Player.php b/PHPSkills/Player.php new file mode 100644 index 0000000..2d44e74 --- /dev/null +++ b/PHPSkills/Player.php @@ -0,0 +1,80 @@ + +/// Represents a player who has a . +/// +class Player implements ISupportPartialPlay, ISupportPartialUpdate +{ + const DEFAULT_PARTIAL_PLAY_PERCENTAGE = 1.0; // = 100% play time + const DEFAULT_PARTIAL_UPDATE_PERCENTAGE = 1.0; // = receive 100% update + + private $_Id; + private $_PartialPlayPercentage; + private $_PartialUpdatePercentage; + + /// + /// Constructs a player. + /// + /// The identifier for the player, such as a name. + /// The weight percentage to give this player when calculating a new rank. + /// /// Indicated how much of a skill update a player should receive where 0 represents no update and 1.0 represents 100% of the update. + public function __construct($id, + $partialPlayPercentage = self::DEFAULT_PARTIAL_PLAY_PERCENTAGE, + $partialUpdatePercentage = self::DEFAULT_PARTIAL_UPDATE_PERCENTAGE) + { + // If they don't want to give a player an id, that's ok... + Guard::argumentInRangeInclusive($partialPlayPercentage, 0.0, 1.0, "partialPlayPercentage"); + Guard::argumentInRangeInclusive($partialUpdatePercentage, 0, 1.0, "partialUpdatePercentage"); + $this->_Id = $id; + $this->_PartialPlayPercentage = $partialPlayPercentage; + $this->_PartialUpdatePercentage = $partialUpdatePercentage; + } + + /// + /// The identifier for the player, such as a name. + /// + public function getId() + { + return $this->_Id; + } + + #region ISupportPartialPlay Members + + /// + /// Indicates the percent of the time the player should be weighted where 0.0 indicates the player didn't play and 1.0 indicates the player played 100% of the time. + /// + public function getPartialPlayPercentage() + { + return $this->_PartialPlayPercentage; + } + + #endregion + + #region ISupportPartialUpdate Members + + /// + /// Indicated how much of a skill update a player should receive where 0.0 represents no update and 1.0 represents 100% of the update. + /// + public function getPartialUpdatePercentage() + { + return $this->_PartialUpdatePercentage; + } + + #endregion + + public function __toString() + { + if ($this->_Id != null) + { + return $this->_Id; + } + + return parent::__toString(); + } +} +?> diff --git a/PHPSkills/PlayersRange.php b/PHPSkills/PlayersRange.php new file mode 100644 index 0000000..7186ce7 --- /dev/null +++ b/PHPSkills/PlayersRange.php @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/PHPSkills/RankSorter.php b/PHPSkills/RankSorter.php new file mode 100644 index 0000000..85c5e16 --- /dev/null +++ b/PHPSkills/RankSorter.php @@ -0,0 +1,23 @@ + +/// Helper class to sort ranks in non-decreasing order. +/// +class RankSorter +{ + /// + /// Performs an in-place sort of the in according to the in non-decreasing order. + /// + /// The types of items to sort. + /// The items to sort according to the order specified by . + /// The ranks for each item where 1 is first place. + public static function sort(array &$teams, array &$teamRanks) + { + array_multisort($teamRanks, $teams); + return $teams; + } +} + +?> \ No newline at end of file diff --git a/PHPSkills/Rating.php b/PHPSkills/Rating.php new file mode 100644 index 0000000..75114b8 --- /dev/null +++ b/PHPSkills/Rating.php @@ -0,0 +1,79 @@ +_mean = $mean; + $this->_standardDeviation = $standardDeviation; + $this->_conservativeStandardDeviationMultiplier = $conservativeStandardDeviationMultiplier; + } + + /** + * The statistical mean value of the rating (also known as �). + */ + public function getMean() + { + return $this->_mean; + } + + /** + * The standard deviation (the spread) of the rating. This is also known as s. + */ + public function getStandardDeviation() + { + return $this->_standardDeviation; + } + + /** + * A conservative estimate of skill based on the mean and standard deviation. + */ + public function getConservativeRating() + { + return $this->_mean - $this->_conservativeStandardDeviationMultiplier*$this->_standardDeviation; + } + + public function getPartialUpdate(Rating $prior, Rating $fullPosterior, $updatePercentage) + { + $priorGaussian = new GaussianDistribution($prior->getMean(), $prior->getStandardDeviation()); + $posteriorGaussian = new GaussianDistribution($fullPosterior->getMean(), $fullPosterior.getStandardDeviation()); + + // From a clarification email from Ralf Herbrich: + // "the idea is to compute a linear interpolation between the prior and posterior skills of each player + // ... in the canonical space of parameters" + + $precisionDifference = $posteriorGaussian->getPrecision() - $priorGaussian->getPrecision(); + $partialPrecisionDifference = $updatePercentage*$precisionDifference; + + $precisionMeanDifference = $posteriorGaussian->getPrecisionMean() - $priorGaussian.getPrecisionMean(); + $partialPrecisionMeanDifference = $updatePercentage*$precisionMeanDifference; + + $partialPosteriorGaussion = GaussianDistribution::fromPrecisionMean( + $priorGaussian->getPrecisionMean() + $partialPrecisionMeanDifference, + $priorGaussian->getPrecision() + $partialPrecisionDifference); + + return new Rating($partialPosteriorGaussion->getMean(), $partialPosteriorGaussion->getStandardDeviation(), $prior->_conservativeStandardDeviationMultiplier); + } + + public function __toString() + { + return 'mean=' . $this->_mean . ' standardDeviation=' . $this->_standardDeviation; + } +} + +?> \ No newline at end of file diff --git a/PHPSkills/RatingContainer.php b/PHPSkills/RatingContainer.php new file mode 100644 index 0000000..382b962 --- /dev/null +++ b/PHPSkills/RatingContainer.php @@ -0,0 +1,46 @@ +_playerHashToRating[self::getHash($player)]; + } + + public function setRating($player, $rating) + { + $hash = self::getHash($player); + $this->_playerHashToPlayer[$hash] = $player; + $this->_playerHashToRating[$hash] = $rating; + return $this; + } + + public function getAllPlayers() + { + return \array_values($this->_playerHashToPlayer); + } + + public function getAllRatings() + { + return \array_values($this->_playerHashToRating); + } + + public function count() + { + return \count($this->_playerHashToPlayer); + } + private static function getHash($player) + { + if(\is_object($player)) + { + return \spl_object_hash($player); + } + + return $player; + } +} +?> diff --git a/PHPSkills/SkillCalculator.php b/PHPSkills/SkillCalculator.php new file mode 100644 index 0000000..6a1e074 --- /dev/null +++ b/PHPSkills/SkillCalculator.php @@ -0,0 +1,81 @@ +_supportedOptions = $supportedOptions; + $this->_totalTeamsAllowed = $totalTeamsAllowed; + $this->_playersPerTeamAllowed = $playerPerTeamAllowed; + } + + /// + /// Calculates new ratings based on the prior ratings and team ranks. + /// + /// The underlying type of the player. + /// Parameters for the game. + /// A mapping of team players and their ratings. + /// The ranks of the teams where 1 is first place. For a tie, repeat the number (e.g. 1, 2, 2) + /// All the players and their new ratings. + public abstract function calculateNewRatings($gameInfo, + array $teamsOfPlayerToRatings, + array $teamRanks); + + /// + /// Calculates the match quality as the likelihood of all teams drawing. + /// + /// The underlying type of the player. + /// Parameters for the game. + /// A mapping of team players and their ratings. + /// The quality of the match between the teams as a percentage (0% = bad, 100% = well matched). + public abstract function calculateMatchQuality($gameInfo, + array $teamsOfPlayerToRatings); + + public function isSupported($option) + { + return ($this->_supportedOptions & $option) == $option; + } + + protected function validateTeamCountAndPlayersCountPerTeam(array $teamsOfPlayerToRatings) + { + self::validateTeamCountAndPlayersCountPerTeamWithRanges($teamsOfPlayerToRatings, $this->_totalTeamsAllowed, $this->_playersPerTeamAllowed); + } + + private static function validateTeamCountAndPlayersCountPerTeamWithRanges( + array $teams, + TeamsRange $totalTeams, + PlayersRange $playersPerTeam) + { + $countOfTeams = 0; + + foreach ($teams as $currentTeam) + { + if (!$playersPerTeam->isInRange($currentTeam->count())) + { + throw new Exception("Player count is not in range"); + } + $countOfTeams++; + } + + if (!$totalTeams->isInRange($countOfTeams)) + { + throw new Exception("Team range is not in range"); + } + } +} + +class SkillCalculatorSupportedOptions +{ + const NONE = 0x00; + const PARTIAL_PLAY = 0x01; + const PARTIAL_UPDATE = 0x02; +} +?> \ No newline at end of file diff --git a/PHPSkills/Team.php b/PHPSkills/Team.php new file mode 100644 index 0000000..f56c425 --- /dev/null +++ b/PHPSkills/Team.php @@ -0,0 +1,24 @@ +addPlayer($player, $rating); + } + } + + public function addPlayer($player, $rating) + { + $this->setRating($player, $rating); + return $this; + } + +} + +?> diff --git a/PHPSkills/Teams.php b/PHPSkills/Teams.php new file mode 100644 index 0000000..a34fd74 --- /dev/null +++ b/PHPSkills/Teams.php @@ -0,0 +1,18 @@ + diff --git a/PHPSkills/TeamsRange.php b/PHPSkills/TeamsRange.php new file mode 100644 index 0000000..d895a3f --- /dev/null +++ b/PHPSkills/TeamsRange.php @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/PHPSkills/TrueSkill/DrawMargin.php b/PHPSkills/TrueSkill/DrawMargin.php new file mode 100644 index 0000000..12eb777 --- /dev/null +++ b/PHPSkills/TrueSkill/DrawMargin.php @@ -0,0 +1,26 @@ + \ No newline at end of file diff --git a/PHPSkills/TrueSkill/TruncatedGaussianCorrectionFunctions.php b/PHPSkills/TrueSkill/TruncatedGaussianCorrectionFunctions.php new file mode 100644 index 0000000..7789b44 --- /dev/null +++ b/PHPSkills/TrueSkill/TruncatedGaussianCorrectionFunctions.php @@ -0,0 +1,135 @@ + + /// The "V" function where the team performance difference is greater than the draw margin. + /// + /// In the reference F# implementation, this is referred to as "the additive + /// correction of a single-sided truncated Gaussian with unit variance." + /// + /// In the paper, it's referred to as just "ε". + /// + public static function vExceedsMarginScaled($teamPerformanceDifference, $drawMargin, $c) + { + return self::vExceedsMargin($teamPerformanceDifference/$c, $drawMargin/$c); + } + + public static function vExceedsMargin($teamPerformanceDifference, $drawMargin) + { + $denominator = GaussianDistribution::cumulativeTo($teamPerformanceDifference - $drawMargin); + + if ($denominator < 2.222758749e-162) + { + return -$teamPerformanceDifference + $drawMargin; + } + + return GaussianDistribution::at($teamPerformanceDifference - $drawMargin)/$denominator; + } + + /// + /// The "W" function where the team performance difference is greater than the draw margin. + /// + /// In the reference F# implementation, this is referred to as "the multiplicative + /// correction of a single-sided truncated Gaussian with unit variance." + /// + /// + /// + /// + public static function wExceedsMarginScaled($teamPerformanceDifference, $drawMargin, $c) + { + return self::wExceedsMargin($teamPerformanceDifference/$c, $drawMargin/$c); + } + + public static function wExceedsMargin($teamPerformanceDifference, $drawMargin) + { + $denominator = GaussianDistribution::cumulativeTo($teamPerformanceDifference - $drawMargin); + + if ($denominator < 2.222758749e-162) + { + if ($teamPerformanceDifference < 0.0) + { + return 1.0; + } + return 0.0; + } + + $vWin = self::vExceedsMargin($teamPerformanceDifference, $drawMargin); + return $vWin*($vWin + $teamPerformanceDifference - $drawMargin); + } + + // the additive correction of a double-sided truncated Gaussian with unit variance + public static function vWithinMarginScaled($teamPerformanceDifference, $drawMargin, $c) + { + return self::vWithinMargin($teamPerformanceDifference/$c, $drawMargin/$c); + } + + // from F#: + public static function vWithinMargin($teamPerformanceDifference, $drawMargin) + { + $teamPerformanceDifferenceAbsoluteValue = abs($teamPerformanceDifference); + $denominator = + GaussianDistribution::cumulativeTo($drawMargin - $teamPerformanceDifferenceAbsoluteValue) - + GaussianDistribution::cumulativeTo(-$drawMargin - $teamPerformanceDifferenceAbsoluteValue); + + if ($denominator < 2.222758749e-162) + { + if ($teamPerformanceDifference < 0.0) + { + return -$teamPerformanceDifference - $drawMargin; + } + + return -$teamPerformanceDifference + $drawMargin; + } + + $numerator = GaussianDistribution::at(-$drawMargin - $teamPerformanceDifferenceAbsoluteValue) - + GaussianDistribution::at($drawMargin - $teamPerformanceDifferenceAbsoluteValue); + + if ($teamPerformanceDifference < 0.0) + { + return -$numerator/$denominator; + } + + return $numerator/$denominator; + } + + // the multiplicative correction of a double-sided truncated Gaussian with unit variance + public static function wWithinMarginScaled($teamPerformanceDifference, $drawMargin, $c) + { + return self::wWithinMargin(teamPerformanceDifference/c, drawMargin/c); + } + + // From F#: + public static function wWithinMargin($teamPerformanceDifference, $drawMargin) + { + $teamPerformanceDifferenceAbsoluteValue = abs($teamPerformanceDifference); + $denominator = GaussianDistribution::cumulativeTo($drawMargin - $teamPerformanceDifferenceAbsoluteValue) + - + GaussianDistribution::cumulativeTo(-$drawMargin - $teamPerformanceDifferenceAbsoluteValue); + + if ($denominator < 2.222758749e-162) + { + return 1.0; + } + + $vt = vWithinMargin($teamPerformanceDifferenceAbsoluteValue, $drawMargin); + + return $vt*$vt + + ( + ($drawMargin - $teamPerformanceDifferenceAbsoluteValue) + * + GaussianDistribution::at( + $drawMargin - $teamPerformanceDifferenceAbsoluteValue) + - (-$drawMargin - $teamPerformanceDifferenceAbsoluteValue) + * + GaussianDistribution::at(-$drawMargin - $teamPerformanceDifferenceAbsoluteValue))/$denominator; + } +} +?> diff --git a/PHPSkills/TrueSkill/TwoPlayerTrueSkillCalculator.php b/PHPSkills/TrueSkill/TwoPlayerTrueSkillCalculator.php new file mode 100644 index 0000000..34928b3 --- /dev/null +++ b/PHPSkills/TrueSkill/TwoPlayerTrueSkillCalculator.php @@ -0,0 +1,173 @@ + +/// Calculates the new ratings for only two players. +/// +/// +/// When you only have two players, a lot of the math simplifies. The main purpose of this class +/// is to show the bare minimum of what a TrueSkill implementation should have. +/// +class TwoPlayerTrueSkillCalculator extends SkillCalculator +{ + public function __construct() + { + parent::__construct(SkillCalculatorSupportedOptions::NONE, TeamsRange::exactly(2), PlayersRange::exactly(1)); + } + + public function calculateNewRatings($gameInfo, + array $teams, + array $teamRanks) + { + // Basic argument checking + $this->validateTeamCountAndPlayersCountPerTeam($teams); + + // Make sure things are in order + RankSorter::sort($teams, $teamRanks); + + // Since we verified that each team has one player, we know the player is the first one + $winningTeamPlayers = $teams[0]->getAllPlayers(); + $winner = $winningTeamPlayers[0]; + $winnerPreviousRating = $teams[0]->getRating($winner); + + $losingTeamPlayers = $teams[1]->getAllPlayers(); + $loser = $losingTeamPlayers[0]; + $loserPreviousRating = $teams[1]->getRating($loser); + + $wasDraw = ($teamRanks[0] == $teamRanks[1]); + + $results = new RatingContainer(); + + $results->setRating($winner, self::calculateNewRating($gameInfo, + $winnerPreviousRating, + $loserPreviousRating, + $wasDraw ? PairwiseComparison::DRAW + : PairwiseComparison::WIN)); + + $results->setRating($loser, self::calculateNewRating($gameInfo, + $loserPreviousRating, + $winnerPreviousRating, + $wasDraw ? PairwiseComparison::DRAW + : PairwiseComparison::LOSE)); + + // And we're done! + return $results; + } + + private static function calculateNewRating($gameInfo, $selfRating, $opponentRating, $comparison) + { + $drawMargin = DrawMargin::getDrawMarginFromDrawProbability($gameInfo->getDrawProbability(), + $gameInfo->getBeta()); + + $c = + sqrt( + square($selfRating->getStandardDeviation()) + + + square($opponentRating->getStandardDeviation()) + + + 2*square($gameInfo->getBeta())); + + $winningMean = $selfRating->getMean(); + $losingMean = $opponentRating->getMean(); + + switch ($comparison) + { + case PairwiseComparison::WIN: + case PairwiseComparison::DRAW: + // NOP + break; + case PairwiseComparison::LOSE: + $winningMean = $opponentRating->getMean(); + $losingMean = $selfRating->getMean(); + break; + } + + $meanDelta = $winningMean - $losingMean; + + if ($comparison != PairwiseComparison::DRAW) + { + // non-draw case + $v = TruncatedGaussianCorrectionFunctions::vExceedsMarginScaled($meanDelta, $drawMargin, $c); + $w = TruncatedGaussianCorrectionFunctions::wExceedsMarginScaled($meanDelta, $drawMargin, $c); + $rankMultiplier = (int) $comparison; + } + else + { + $v = TruncatedGaussianCorrectionFunctions::vWithinMarginScaled($meanDelta, $drawMargin, $c); + $w = TruncatedGaussianCorrectionFunctions::wWithinMarginScaled($meanDelta, $drawMargin, $c); + $rankMultiplier = 1; + } + + $meanMultiplier = (square($selfRating->getStandardDeviation()) + square($gameInfo->getDynamicsFactor()))/$c; + + $varianceWithDynamics = square($selfRating->getStandardDeviation()) + square($gameInfo->getDynamicsFactor()); + $stdDevMultiplier = $varianceWithDynamics/square($c); + + $newMean = $selfRating->getMean() + ($rankMultiplier*$meanMultiplier*$v); + $newStdDev = sqrt($varianceWithDynamics*(1 - $w*$stdDevMultiplier)); + + return new Rating($newMean, $newStdDev); + } + + /// + public function calculateMatchQuality($gameInfo, array $teams) + { + $this->validateTeamCountAndPlayersCountPerTeam($teams); + + $team1 = $teams[0]; + $team2 = $teams[1]; + + $team1Ratings = $team1->getAllRatings(); + $team2Ratings = $team2->getAllRatings(); + + $player1Rating = $team1Ratings[0]; + $player2Rating = $team2Ratings[0]; + + // We just use equation 4.1 found on page 8 of the TrueSkill 2006 paper: + $betaSquared = square($gameInfo->getBeta()); + $player1SigmaSquared = square($player1Rating->getStandardDeviation()); + $player2SigmaSquared = square($player2Rating->getStandardDeviation()); + + // This is the square root part of the equation: + $sqrtPart = + sqrt( + (2*$betaSquared) + / + (2*$betaSquared + $player1SigmaSquared + $player2SigmaSquared)); + + // This is the exponent part of the equation: + $expPart = + exp( + (-1*square($player1Rating->getMean() - $player2Rating->getMean())) + / + (2*(2*$betaSquared + $player1SigmaSquared + $player2SigmaSquared))); + + return $sqrtPart*$expPart; + } +} +?> diff --git a/UnitTests/Elo/EloAssert.php b/UnitTests/Elo/EloAssert.php new file mode 100644 index 0000000..c0e3277 --- /dev/null +++ b/UnitTests/Elo/EloAssert.php @@ -0,0 +1,46 @@ + new EloRating($player1BeforeRating) ), + array( $player2 => new EloRating($player2BeforeRating) ) + ); + + $chessGameInfo = new GameInfo(1200, 0, 200); + + $ranks = PairwiseComparison::getRankFromComparison($player1Result); + + $result = $twoPlayerEloCalculator->calculateNewRatings( + $chessGameInfo, + $teams, + $ranks); + + $testClass->assertEquals($player1AfterRating, $result[$player1]->getMean(), '', self::ERROR_TOLERANCE); + $testClass->assertEquals($player2AfterRating, $result[$player2]->getMean(), '', self::ERROR_TOLERANCE); + } +} +?> + diff --git a/UnitTests/Elo/FideEloCalculatorTest.php b/UnitTests/Elo/FideEloCalculatorTest.php new file mode 100644 index 0000000..fe894ff --- /dev/null +++ b/UnitTests/Elo/FideEloCalculatorTest.php @@ -0,0 +1,39 @@ + + diff --git a/UnitTests/Numerics/BasicMathTest.php b/UnitTests/Numerics/BasicMathTest.php new file mode 100644 index 0000000..bfcbc2d --- /dev/null +++ b/UnitTests/Numerics/BasicMathTest.php @@ -0,0 +1,15 @@ +assertEquals( 1, Moserware\Numerics\square(1) ); + $this->assertEquals( 1.44, Moserware\Numerics\square(1.2) ); + $this->assertEquals( 4, Moserware\Numerics\square(2) ); + } +} +?> \ No newline at end of file diff --git a/UnitTests/Numerics/GaussianDistributionTest.php b/UnitTests/Numerics/GaussianDistributionTest.php new file mode 100644 index 0000000..86dc10a --- /dev/null +++ b/UnitTests/Numerics/GaussianDistributionTest.php @@ -0,0 +1,106 @@ +assertEquals( 0.691462, GaussianDistribution::cumulativeTo(0.5),'', GaussianDistributionTest::ERROR_TOLERANCE); + } + + public function testAt() + { + // Verified with WolframAlpha + // (e.g. http://www.wolframalpha.com/input/?i=PDF%5BNormalDistribution%5B0%2C1%5D%2C+0.5%5D ) + $this->assertEquals(0.352065, GaussianDistribution::at(0.5), '', GaussianDistributionTest::ERROR_TOLERANCE); + } + + public function testMultiplication() + { + // I verified this against the formula at http://www.tina-vision.net/tina-knoppix/tina-memo/2003-003.pdf + $standardNormal = new GaussianDistribution(0, 1); + $shiftedGaussian = new GaussianDistribution(2, 3); + $product = GaussianDistribution::multiply($standardNormal, $shiftedGaussian); + + $this->assertEquals(0.2, $product->getMean(), '', GaussianDistributionTest::ERROR_TOLERANCE); + $this->assertEquals(3.0 / sqrt(10), $product->getStandardDeviation(), '', GaussianDistributionTest::ERROR_TOLERANCE); + + $m4s5 = new GaussianDistribution(4, 5); + $m6s7 = new GaussianDistribution(6, 7); + + $product2 = GaussianDistribution::multiply($m4s5, $m6s7); + + $expectedMean = (4 * square(7) + 6 * square(5)) / (square(5) + square(7)); + $this->assertEquals($expectedMean, $product2->getMean(), '', GaussianDistributionTest::ERROR_TOLERANCE); + + $expectedSigma = sqrt(((square(5) * square(7)) / (square(5) + square(7)))); + $this->assertEquals($expectedSigma, $product2->getStandardDeviation(), '', GaussianDistributionTest::ERROR_TOLERANCE); + } + + public function testDivision() + { + // Since the multiplication was worked out by hand, we use the same numbers but work backwards + $product = new GaussianDistribution(0.2, 3.0 / sqrt(10)); + $standardNormal = new GaussianDistribution(0, 1); + + $productDividedByStandardNormal = GaussianDistribution::divide($product, $standardNormal); + $this->assertEquals(2.0, $productDividedByStandardNormal->getMean(), '', GaussianDistributionTest::ERROR_TOLERANCE); + $this->assertEquals(3.0, $productDividedByStandardNormal->getStandardDeviation(),'', GaussianDistributionTest::ERROR_TOLERANCE); + + $product2 = new GaussianDistribution((4 * square(7) + 6 * square(5)) / (square(5) + square(7)), sqrt(((square(5) * square(7)) / (square(5) + square(7))))); + $m4s5 = new GaussianDistribution(4,5); + $product2DividedByM4S5 = GaussianDistribution::divide($product2, $m4s5); + $this->assertEquals(6.0, $product2DividedByM4S5->getMean(), '', GaussianDistributionTest::ERROR_TOLERANCE); + $this->assertEquals(7.0, $product2DividedByM4S5->getStandardDeviation(), '', GaussianDistributionTest::ERROR_TOLERANCE); + } + + public function testLogProductNormalization() + { + // Verified with Ralf Herbrich's F# implementation + $standardNormal = new GaussianDistribution(0, 1); + $lpn = GaussianDistribution::logProductNormalization($standardNormal, $standardNormal); + $this->assertEquals(-1.2655121234846454, $lpn, '', GaussianDistributionTest::ERROR_TOLERANCE); + + $m1s2 = new GaussianDistribution(1, 2); + $m3s4 = new GaussianDistribution(3, 4); + $lpn2 = GaussianDistribution::logProductNormalization($m1s2, $m3s4); + $this->assertEquals(-2.5168046699816684, $lpn2, '', GaussianDistributionTest::ERROR_TOLERANCE); + } + + public function testLogRatioNormalization() + { + // Verified with Ralf Herbrich's F# implementation + $m1s2 = new GaussianDistribution(1, 2); + $m3s4 = new GaussianDistribution(3, 4); + $lrn = GaussianDistribution::logRatioNormalization($m1s2, $m3s4); + $this->assertEquals(2.6157405972171204, $lrn, '', GaussianDistributionTest::ERROR_TOLERANCE); + } + + public function testAbsoluteDifference() + { + // Verified with Ralf Herbrich's F# implementation + $standardNormal = new GaussianDistribution(0, 1); + $absDiff = GaussianDistribution::absoluteDifference($standardNormal, $standardNormal); + $this->assertEquals(0.0, $absDiff, '', GaussianDistributionTest::ERROR_TOLERANCE); + + $m1s2 = new GaussianDistribution(1, 2); + $m3s4 = new GaussianDistribution(3, 4); + $absDiff2 = GaussianDistribution::absoluteDifference($m1s2, $m3s4); + $this->assertEquals(0.4330127018922193, $absDiff2, '', GaussianDistributionTest::ERROR_TOLERANCE); + } +} + +?> + diff --git a/UnitTests/RankSorterTest.php b/UnitTests/RankSorterTest.php new file mode 100644 index 0000000..f6de596 --- /dev/null +++ b/UnitTests/RankSorterTest.php @@ -0,0 +1,33 @@ + 1, "b" => 2 ); + $team2 = array( "c" => 3, "d" => 4 ); + $team3 = array( "e" => 5, "f" => 6 ); + + $teams = array($team1, $team2, $team3); + + $teamRanks = array(3, 1, 2); + + $sortedRanks = RankSorter::sort($teams, $teamRanks); + + $this->assertEquals($team2, $sortedRanks[0]); + $this->assertEquals($team3, $sortedRanks[1]); + $this->assertEquals($team1, $sortedRanks[2]); + + } +} + +?> \ No newline at end of file diff --git a/UnitTests/TrueSkill/DrawMarginTest.php b/UnitTests/TrueSkill/DrawMarginTest.php new file mode 100644 index 0000000..0e02562 --- /dev/null +++ b/UnitTests/TrueSkill/DrawMarginTest.php @@ -0,0 +1,36 @@ +assertDrawMargin(0.10, $beta, 0.74046637542690541); + $this->assertDrawMargin(0.25, $beta, 1.87760059883033); + $this->assertDrawMargin(0.33, $beta, 2.5111010132487492); + } + + private function assertDrawMargin($drawProbability, $beta, $expected) + { + $actual = DrawMargin::getDrawMarginFromDrawProbability($drawProbability, $beta); + $this->assertEquals($expected, $actual, '', DrawMarginTest::ERROR_TOLERANCE); + } +} + +$testSuite = new \PHPUnit_Framework_TestSuite(); +$testSuite->addTest( new DrawMarginTest( "testGetDrawMarginFromDrawProbability" ) ); +\PHPUnit_TextUI_TestRunner::run($testSuite); + +?> + diff --git a/UnitTests/TrueSkill/TrueSkillCalculatorTests.php b/UnitTests/TrueSkill/TrueSkillCalculatorTests.php new file mode 100644 index 0000000..d420ddd --- /dev/null +++ b/UnitTests/TrueSkill/TrueSkillCalculatorTests.php @@ -0,0 +1,76 @@ +getDefaultRating()); + $team2 = new Team($player2, $gameInfo->getDefaultRating());; + $teams = Teams::concat($team1, $team2); + + $newRatings = $calculator->calculateNewRatings($gameInfo, $teams, array(1, 2)); + + $player1NewRating = $newRatings->getRating($player1); + self::assertRating($testClass, 29.39583201999924, 7.171475587326186, $player1NewRating); + + $player2NewRating = $newRatings->getRating($player2); + self::assertRating($testClass, 20.60416798000076, 7.171475587326186, $player2NewRating); + + self::assertMatchQuality($testClass, 0.447, $calculator->calculateMatchQuality($gameInfo, $teams)); + } + + private static function assertRating($testClass, $expectedMean, $expectedStandardDeviation, $actual) + { + $testClass->assertEquals($expectedMean, $actual->getMean(), '', self::ERROR_TOLERANCE_TRUESKILL); + $testClass->assertEquals($expectedStandardDeviation, $actual->getStandardDeviation(), '', self::ERROR_TOLERANCE_TRUESKILL); + } + + private static function assertMatchQuality($testClass, $expectedMatchQuality, $actualMatchQuality) + { + $testClass->assertEquals($expectedMatchQuality, $actualMatchQuality, '', self::ERROR_TOLERANCE_MATCH_QUALITY); + } +} + +?> diff --git a/UnitTests/TrueSkill/TwoPlayerTrueSkillCalculatorTest.php b/UnitTests/TrueSkill/TwoPlayerTrueSkillCalculatorTest.php new file mode 100644 index 0000000..0d501db --- /dev/null +++ b/UnitTests/TrueSkill/TwoPlayerTrueSkillCalculatorTest.php @@ -0,0 +1,26 @@ +addTest( new TwoPlayerTrueSkillCalculatorTest("testTwoPlayerTrueSkillCalculator")); + +\PHPUnit_TextUI_TestRunner::run($testSuite); +?> diff --git a/UnitTests/runner_example.php b/UnitTests/runner_example.php new file mode 100644 index 0000000..912bb3e --- /dev/null +++ b/UnitTests/runner_example.php @@ -0,0 +1,33 @@ +require_once 'PHPUnit/Framework.php'; +require_once 'PHPUnit/TextUI/TestRunner.php'; + +require_once(dirname(__FILE__) . '/../PHPSkills/RankSorter.php'); + + +use \PHPUnit_Framework_TestCase; + +class RankSorterTest extends PHPUnit_Framework_TestCase +{ + public function testSort() + { + $team1 = array( "a" => 1, "b" => 2 ); + $team2 = array( "c" => 3, "d" => 4 ); + $team3 = array( "e" => 5, "f" => 6 ); + + $teams = array($team1, $team2, $team3); + + $teamRanks = array(3, 1, 2); + + $sortedRanks = RankSorter::sort($teams, $teamRanks); + + $this->assertEquals($team2, $sortedRanks[0]); + $this->assertEquals($team3, $sortedRanks[1]); + $this->assertEquals($team1, $sortedRanks[2]); + + } +} + +$testSuite = new \PHPUnit_Framework_TestSuite(); +$testSuite->addTest( new RankSorterTest("testSort")); + +\PHPUnit_TextUI_TestRunner::run($testSuite); \ No newline at end of file diff --git a/nbproject/private/private.properties b/nbproject/private/private.properties new file mode 100644 index 0000000..1378646 --- /dev/null +++ b/nbproject/private/private.properties @@ -0,0 +1,5 @@ +copy.src.files=false +copy.src.target= +index.file= +run.as=LOCAL +url=http://localhost/ diff --git a/nbproject/private/private.xml b/nbproject/private/private.xml new file mode 100644 index 0000000..c1f155a --- /dev/null +++ b/nbproject/private/private.xml @@ -0,0 +1,4 @@ + + + + diff --git a/nbproject/project.properties b/nbproject/project.properties new file mode 100644 index 0000000..94429c9 --- /dev/null +++ b/nbproject/project.properties @@ -0,0 +1,7 @@ +include.path=${php.global.include.path} +php.version=PHP_53 +source.encoding=UTF-8 +src.dir=. +tags.asp=false +tags.short=true +web.root=. diff --git a/nbproject/project.xml b/nbproject/project.xml new file mode 100644 index 0000000..1d5a5e0 --- /dev/null +++ b/nbproject/project.xml @@ -0,0 +1,9 @@ + + + org.netbeans.modules.php.project + + + phpskills + + + diff --git a/test.php b/test.php new file mode 100644 index 0000000..f963417 --- /dev/null +++ b/test.php @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/web.config b/web.config new file mode 100644 index 0000000..b7cae15 --- /dev/null +++ b/web.config @@ -0,0 +1,11 @@ + + + + + + + + + + +