First TwoPlayerTrueSkillCalculator unit test passed

This commit is contained in:
Jeff Moser 2010-08-28 22:05:41 -04:00
commit 12a02b8403
41 changed files with 1909 additions and 0 deletions

@ -0,0 +1,19 @@
<?php
namespace Moserware\Skills\Elo;
require_once(dirname(__FILE__) . '/../Rating.php');
use Moserware\Skills\Rating;
/**
* An Elo rating represented by a single number (mean).
*/
class EloRating extends Rating
{
public function __construct($rating)
{
parent::__construct($rating, 0);
}
}
?>

@ -0,0 +1,41 @@
<?php
namespace Moserware\Skills\Elo;
require_once(dirname(__FILE__) . "/TwoPlayerEloCalculator.php");
require_once(dirname(__FILE__) . "/FideKFactor.php");
/** Including ELO's scheme as a simple comparison.
* See http://en.wikipedia.org/wiki/Elo_rating_system#Theory
* for more details
*/
class FideEloCalculator extends TwoPlayerEloCalculator
{
public function __construct(FideKFactor $kFactor)
{
parent::__construct($kFactor);
}
public static function createWithDefaultKFactor()
{
return new FideEloCalculator(new FideKFactor());
}
public static function createWithProvisionalKFactor()
{
return new FideEloCalculator(new ProvisionalFideKFactor());
}
public function getPlayerWinProbability($gameInfo, $playerRating, $opponentRating)
{
$ratingDifference = $opponentRating - $playerRating;
return 1.0
/
(
1.0 + pow(10.0, $ratingDifference / (2 * $gameInfo->getBeta()))
);
}
}
?>

@ -0,0 +1,30 @@
<?php
namespace Moserware\Skills\Elo;
require_once(dirname(__FILE__) . "/KFactor.php");
// see http://ratings.fide.com/calculator_rtd.phtml for details
class FideKFactor extends KFactor
{
public function getValueForRating($rating)
{
if ($rating < 2400)
{
return 15;
}
return 10;
}
}
/**
* Indicates someone who has played less than 30 games.
*/
class ProvisionalFideKFactor extends FideKFactor
{
public function getValueForRating($rating)
{
return 25;
}
}
?>

@ -0,0 +1,26 @@
<?php
namespace Moserware\Skills\Elo;
class GaussianEloCalculator extends TwoPlayerEloCalculator
{
// From the paper
const STABLE_KFACTOR = 24;
public function __construct()
{
parent::__construct(new KFactor(self::STABLE_KFACTOR));
}
public function getPlayerWinProbability(GameInfo $gameInfo, $playerRating, $opponentRating)
{
$ratingDifference = $playerRating - $opponentRating;
// See equation 1.1 in the TrueSkill paper
return GaussianDistribution::cumulativeTo(
$ratingDifference
/
(sqrt(2) * $gameInfo->getBeta()));
}
}
?>

22
PHPSkills/Elo/KFactor.php Normal file

@ -0,0 +1,22 @@
<?php
namespace Moserware\Skills\Elo;
class KFactor
{
const DEFAULT_KFACTOR = 24;
private $_value;
public function __construct($exactKFactor = self::DEFAULT_KFACTOR)
{
$this->_value = $exactKFactor;
}
public function getValueForRating($rating)
{
return $this->_value;
}
}
?>

@ -0,0 +1,105 @@
<?php
namespace Moserware\Skills\Elo;
require_once(dirname(__FILE__) . "/../PairwiseComparison.php");
require_once(dirname(__FILE__) . "/../RankSorter.php");
require_once(dirname(__FILE__) . "/../SkillCalculator.php");
require_once(dirname(__FILE__) . "/../PlayersRange.php");
require_once(dirname(__FILE__) . "/../TeamsRange.php");
use Moserware\Skills\PairwiseComparison;
use Moserware\Skills\RankSorter;
use Moserware\Skills\SkillCalculator;
use Moserware\Skills\SkillCalculatorSupportedOptions;
use Moserware\Skills\PlayersRange;
use Moserware\Skills\TeamsRange;
abstract class TwoPlayerEloCalculator extends SkillCalculator
{
protected $_kFactor;
protected function __construct(KFactor $kFactor)
{
parent::__construct(SkillCalculatorSupportedOptions::NONE, TeamsRange::exactly(2), PlayersRange::exactly(1));
$this->_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;
}
}
?>

70
PHPSkills/GameInfo.php Normal file

@ -0,0 +1,70 @@
<?php
namespace Moserware\Skills;
require_once(dirname(__FILE__) . "/Rating.php");
/**
* Parameters about the game for calculating the TrueSkill.
*/
class GameInfo
{
const DEFAULT_BETA = 4.1666666666666666666666666666667; // Default initial mean / 6
const DEFAULT_DRAW_PROBABILITY = 0.10;
const DEFAULT_DYNAMICS_FACTOR = 0.083333333333333333333333333333333; // Default initial mean / 300
const DEFAULT_INITIAL_MEAN = 25.0;
const DEFAULT_INITIAL_STANDARD_DEVIATION = 8.3333333333333333333333333333333; // Default initial mean / 3
private $_initialMean;
private $_initialStandardDeviation;
private $_beta;
private $_dynamicsFactor;
private $_drawProbability;
public function __construct($initialMean = self::DEFAULT_INITIAL_MEAN,
$initialStandardDeviation = self::DEFAULT_INITIAL_STANDARD_DEVIATION,
$beta = self::DEFAULT_BETA,
$dynamicsFactor = self::DEFAULT_DYNAMICS_FACTOR,
$drawProbability = self::DEFAULT_DRAW_PROBABILITY)
{
$this->_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);
}
}
?>

35
PHPSkills/Guard.php Normal file

@ -0,0 +1,35 @@
<?php
namespace Moserware\Skills;
/// <summary>
/// Verifies argument contracts.
/// </summary>
/// <remarks>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</remarks>
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 . "]");
}
}
}
?>

@ -0,0 +1,16 @@
<?php
namespace Moserware\Skills;
/**
* Indicates support for allowing partial play (where a player only plays a part of the time).
*/
interface ISupportPartialPlay
{
/**
* 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();
}
?>

@ -0,0 +1,11 @@
<?php
namespace Moserware\Skills;
interface ISupportPartialUpdate
{
/**
* 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();
}
?>

@ -0,0 +1,17 @@
<?php
/**
* Basic math functions.
*
* PHP version 5
*
* @category Math
* @package PHPSkills
* @author Jeff Moser <jeff@moserware.com>
* @copyright 2010 Jeff Moser
*/
function square($x)
{
return $x * $x;
}
?>

@ -0,0 +1,247 @@
<?php
/**
* Computes Gaussian values.
*
* PHP version 5
*
* @category Math
* @package PHPSkills
* @author Jeff Moser <jeff@moserware.com>
* @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;
}
}
?>

@ -0,0 +1,62 @@
<?php
namespace Moserware\Numerics;
// The whole purpose of this class is to make the code for the SkillCalculator(s)
// look a little cleaner
class Range
{
private $_min;
private $_max;
public function __construct($min, $max)
{
if ($min > $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);
}
}
?>

@ -0,0 +1,27 @@
<?php
namespace Moserware\Skills;
/**
* Represents a comparison between two players.
* @internal The actual values for the enum were chosen so that the also correspond to the multiplier for updates to means.
*/
class PairwiseComparison
{
const WIN = 1;
const DRAW = 0;
const LOSE = -1;
public static function getRankFromComparison($comparison)
{
switch ($comparison) {
case PairwiseComparison::WIN:
return array(1,2);
case PairwiseComparison::LOSE:
return array(2,1);
default:
return array(1,1);
}
}
}
?>

80
PHPSkills/Player.php Normal file

@ -0,0 +1,80 @@
<?php
namespace Moserware\Skills;
require_once(dirname(__FILE__) . "/Guard.php");
require_once(dirname(__FILE__) . "/ISupportPartialPlay.php");
require_once(dirname(__FILE__) . "/ISupportPartialUpdate.php");
/// <summary>
/// Represents a player who has a <see cref="Rating"/>.
/// </summary>
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;
/// <summary>
/// Constructs a player.
/// </summary>
/// <param name="id">The identifier for the player, such as a name.</param>
/// <param name="partialPlayPercentage">The weight percentage to give this player when calculating a new rank.</param>
/// <param name="partialUpdatePercentage">/// Indicated how much of a skill update a player should receive where 0 represents no update and 1.0 represents 100% of the update.</param>
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;
}
/// <summary>
/// The identifier for the player, such as a name.
/// </summary>
public function getId()
{
return $this->_Id;
}
#region ISupportPartialPlay Members
/// <summary>
/// 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.
/// </summary>
public function getPartialPlayPercentage()
{
return $this->_PartialPlayPercentage;
}
#endregion
#region ISupportPartialUpdate Members
/// <summary>
/// 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.
/// </summary>
public function getPartialUpdatePercentage()
{
return $this->_PartialUpdatePercentage;
}
#endregion
public function __toString()
{
if ($this->_Id != null)
{
return $this->_Id;
}
return parent::__toString();
}
}
?>

@ -0,0 +1,21 @@
<?php
namespace Moserware\Skills;
require_once(dirname(__FILE__) . "/Numerics/Range.php");
use Moserware\Numerics\Range;
class PlayersRange extends Range
{
public function __construct($min, $max)
{
parent::__construct($min, $max);
}
protected static function create($min, $max)
{
return new PlayersRange($min, $max);
}
}
?>

23
PHPSkills/RankSorter.php Normal file

@ -0,0 +1,23 @@
<?php
namespace Moserware\Skills;
/// <summary>
/// Helper class to sort ranks in non-decreasing order.
/// </summary>
class RankSorter
{
/// <summary>
/// Performs an in-place sort of the <paramref name="items"/> in according to the <paramref name="ranks"/> in non-decreasing order.
/// </summary>
/// <typeparam name="T">The types of items to sort.</typeparam>
/// <param name="items">The items to sort according to the order specified by <paramref name="ranks"/>.</param>
/// <param name="ranks">The ranks for each item where 1 is first place.</param>
public static function sort(array &$teams, array &$teamRanks)
{
array_multisort($teamRanks, $teams);
return $teams;
}
}
?>

79
PHPSkills/Rating.php Normal file

@ -0,0 +1,79 @@
<?php
namespace Moserware\Skills;
// Container for a player's rating.
class Rating
{
const CONSERVATIVE_STANDARD_DEVIATION_MULTIPLIER = 3;
private $_conservativeStandardDeviationMultiplier;
private $_mean;
private $_standardDeviation;
/**
* Constructs a rating.
* @param double $mean The statistical mean value of the rating (also known as mu).
* @param double $standardDeviation The standard deviation of the rating (also known as s).
* @param double $conservativeStandardDeviationMultiplier optional The number of standardDeviations to subtract from the mean to achieve a conservative rating.
*/
public function __construct($mean, $standardDeviation, $conservativeStandardDeviationMultiplier = self::CONSERVATIVE_STANDARD_DEVIATION_MULTIPLIER)
{
$this->_mean = $mean;
$this->_standardDeviation = $standardDeviation;
$this->_conservativeStandardDeviationMultiplier = $conservativeStandardDeviationMultiplier;
}
/**
* The statistical mean value of the rating (also known as <EFBFBD>).
*/
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;
}
}
?>

@ -0,0 +1,46 @@
<?php
namespace Moserware\Skills;
class RatingContainer
{
private $_playerHashToRating = array();
private $_playerHashToPlayer = array();
public function getRating($player)
{
return $this->_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;
}
}
?>

@ -0,0 +1,81 @@
<?php
namespace Moserware\Skills;
/**
* Base class for all skill calculator implementations.
*/
abstract class SkillCalculator
{
private $_supportedOptions;
private $_playersPerTeamAllowed;
private $_totalTeamsAllowed;
protected function __construct($supportedOptions, TeamsRange $totalTeamsAllowed, PlayersRange $playerPerTeamAllowed)
{
$this->_supportedOptions = $supportedOptions;
$this->_totalTeamsAllowed = $totalTeamsAllowed;
$this->_playersPerTeamAllowed = $playerPerTeamAllowed;
}
/// <summary>
/// Calculates new ratings based on the prior ratings and team ranks.
/// </summary>
/// <typeparam name="TPlayer">The underlying type of the player.</typeparam>
/// <param name="gameInfo">Parameters for the game.</param>
/// <param name="teams">A mapping of team players and their ratings.</param>
/// <param name="teamRanks">The ranks of the teams where 1 is first place. For a tie, repeat the number (e.g. 1, 2, 2)</param>
/// <returns>All the players and their new ratings.</returns>
public abstract function calculateNewRatings($gameInfo,
array $teamsOfPlayerToRatings,
array $teamRanks);
/// <summary>
/// Calculates the match quality as the likelihood of all teams drawing.
/// </summary>
/// <typeparam name="TPlayer">The underlying type of the player.</typeparam>
/// <param name="gameInfo">Parameters for the game.</param>
/// <param name="teams">A mapping of team players and their ratings.</param>
/// <returns>The quality of the match between the teams as a percentage (0% = bad, 100% = well matched).</returns>
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;
}
?>

24
PHPSkills/Team.php Normal file

@ -0,0 +1,24 @@
<?php
namespace Moserware\Skills;
require_once(dirname(__FILE__) . '/RatingContainer.php');
class Team extends RatingContainer
{
public function __construct($player = null, $rating = null)
{
if(!\is_null($player))
{
$this->addPlayer($player, $rating);
}
}
public function addPlayer($player, $rating)
{
$this->setRating($player, $rating);
return $this;
}
}
?>

18
PHPSkills/Teams.php Normal file

@ -0,0 +1,18 @@
<?php
namespace Moserware\Skills;
class Teams
{
public static function concat(/*variable arguments*/)
{
$args = \func_get_args();
$result = array();
foreach ($args as $currentTeam) {
$result[] = $currentTeam;
}
return $result;
}
}
?>

21
PHPSkills/TeamsRange.php Normal file

@ -0,0 +1,21 @@
<?php
namespace Moserware\Skills;
require_once(dirname(__FILE__) . "/Numerics/Range.php");
use Moserware\Numerics\Range;
class TeamsRange extends Range
{
public function __construct($min, $max)
{
parent::__construct($min, $max);
}
protected static function create($min, $max)
{
return new TeamsRange($min, $max);
}
}
?>

@ -0,0 +1,26 @@
<?php
namespace Moserware\Skills\TrueSkill;
require_once(dirname(__FILE__) . "/../Numerics/GaussianDistribution.php");
use Moserware\Numerics\GaussianDistribution;
final class DrawMargin
{
public static function getDrawMarginFromDrawProbability($drawProbability, $beta)
{
// Derived from TrueSkill technical report (MSR-TR-2006-80), page 6
// draw probability = 2 * CDF(margin/(sqrt(n1+n2)*beta)) -1
// implies
//
// margin = inversecdf((draw probability + 1)/2) * sqrt(n1+n2) * beta
// n1 and n2 are the number of players on each team
$margin = GaussianDistribution::inverseCumulativeTo(.5*($drawProbability + 1), 0, 1)*sqrt(1 + 1)*
$beta;
return $margin;
}
}
?>

@ -0,0 +1,135 @@
<?php
namespace Moserware\Skills\TrueSkill;
require_once(dirname(__FILE__) . '/../Numerics/GaussianDistribution.php');
use Moserware\Numerics\GaussianDistribution;
class TruncatedGaussianCorrectionFunctions
{
// These functions from the bottom of page 4 of the TrueSkill paper.
/// <summary>
/// The "V" function where the team performance difference is greater than the draw margin.
/// </summary>
/// <remarks>In the reference F# implementation, this is referred to as "the additive
/// correction of a single-sided truncated Gaussian with unit variance."</remarks>
/// <param name="teamPerformanceDifference"></param>
/// <param name="drawMargin">In the paper, it's referred to as just "ε".</param>
/// <returns></returns>
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;
}
/// <summary>
/// The "W" function where the team performance difference is greater than the draw margin.
/// </summary>
/// <remarks>In the reference F# implementation, this is referred to as "the multiplicative
/// correction of a single-sided truncated Gaussian with unit variance."</remarks>
/// <param name="teamPerformanceDifference"></param>
/// <param name="drawMargin"></param>
/// <param name="c"></param>
/// <returns></returns>
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;
}
}
?>

@ -0,0 +1,173 @@
<?php
namespace Moserware\Skills\TrueSkill;
require_once(dirname(__FILE__) . "/../PairwiseComparison.php");
require_once(dirname(__FILE__) . "/../RankSorter.php");
require_once(dirname(__FILE__) . "/../Rating.php");
require_once(dirname(__FILE__) . "/../RatingContainer.php");
require_once(dirname(__FILE__) . "/../SkillCalculator.php");
require_once(dirname(__FILE__) . "/../PlayersRange.php");
require_once(dirname(__FILE__) . "/../TeamsRange.php");
require_once(dirname(__FILE__) . "/../Numerics/BasicMath.php");
require_once(dirname(__FILE__) . "/DrawMargin.php");
require_once(dirname(__FILE__) . "/TruncatedGaussianCorrectionFunctions.php");
use Moserware\Skills\PairwiseComparison;
use Moserware\Skills\RankSorter;
use Moserware\Skills\Rating;
use Moserware\Skills\RatingContainer;
use Moserware\Skills\SkillCalculator;
use Moserware\Skills\SkillCalculatorSupportedOptions;
use Moserware\Skills\PlayersRange;
use Moserware\Skills\TeamsRange;
/// <summary>
/// Calculates the new ratings for only two players.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
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);
}
/// <inheritdoc/>
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;
}
}
?>

@ -0,0 +1,46 @@
<?php
namespace Moserware\Skills\Elo;
require_once(dirname(__FILE__) . '/../../PHPSkills/Elo/EloRating.php');
require_once(dirname(__FILE__) . '/../../PHPSkills/GameInfo.php');
require_once(dirname(__FILE__) . '/../../PHPSkills/PairwiseComparison.php');
use Moserware\Skills\GameInfo;
use Moserware\Skills\PairwiseComparison;
class EloAssert
{
const ERROR_TOLERANCE = 0.1;
public static function assertChessRating(
$testClass,
$twoPlayerEloCalculator,
$player1BeforeRating,
$player2BeforeRating,
$player1Result,
$player1AfterRating,
$player2AfterRating)
{
$player1 = "Player1";
$player2 = "Player2";
$teams = array(
array( $player1 => 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);
}
}
?>

@ -0,0 +1,39 @@
<?php
namespace Moserware\Skills\Elo;
require_once(dirname(__FILE__) . '/EloAssert.php');
require_once(dirname(__FILE__) . '/../../PHPSkills/PairwiseComparison.php');
require_once(dirname(__FILE__) . '/../../PHPSkills/Elo/FideEloCalculator.php');
require_once(dirname(__FILE__) . '/../../PHPSkills/Elo/FideKFactor.php');
use Moserware\Skills\PairwiseComparison;
use \PHPUnit_Framework_TestCase;
class FideEloCalculatorTest extends PHPUnit_Framework_TestCase
{
public function testFideProvisionalEloCalculator()
{
// verified against http://ratings.fide.com/calculator_rtd.phtml
$calc = new FideEloCalculator(new ProvisionalFideKFactor());
EloAssert::assertChessRating($this, $calc, 1200, 1500, PairwiseComparison::WIN, 1221.25, 1478.75);
EloAssert::assertChessRating($this, $calc, 1200, 1500, PairwiseComparison::DRAW, 1208.75, 1491.25);
EloAssert::assertChessRating($this, $calc, 1200, 1500, PairwiseComparison::LOSE, 1196.25, 1503.75);
}
public function testFideNonProvisionalEloCalculator()
{
// verified against http://ratings.fide.com/calculator_rtd.phtml
$calc = FideEloCalculator::createWithDefaultKFactor();
EloAssert::assertChessRating($this, $calc, 1200, 1200, PairwiseComparison::WIN, 1207.5, 1192.5);
EloAssert::assertChessRating($this, $calc, 1200, 1200, PairwiseComparison::DRAW, 1200, 1200);
EloAssert::assertChessRating($this, $calc, 1200, 1200, PairwiseComparison::LOSE, 1192.5, 1207.5);
EloAssert::assertChessRating($this, $calc, 2600, 2500, PairwiseComparison::WIN, 2603.6, 2496.4);
EloAssert::assertChessRating($this, $calc, 2600, 2500, PairwiseComparison::DRAW, 2598.6, 2501.4);
EloAssert::assertChessRating($this, $calc, 2600, 2500, PairwiseComparison::LOSE, 2593.6, 2506.4);
}
}
?>

@ -0,0 +1,15 @@
<?php
require_once 'PHPUnit/Framework.php';
require_once(dirname(__FILE__) . '/../../PHPSkills/Numerics/BasicMath.php');
class BasicMathTest extends PHPUnit_Framework_TestCase
{
public function testSquare()
{
$this->assertEquals( 1, Moserware\Numerics\square(1) );
$this->assertEquals( 1.44, Moserware\Numerics\square(1.2) );
$this->assertEquals( 4, Moserware\Numerics\square(2) );
}
}
?>

@ -0,0 +1,106 @@
<?php
namespace Moserware\Numerics;
require_once 'PHPUnit/Framework.php';
require_once 'PHPUnit/TextUI/TestRunner.php';
require_once(dirname(__FILE__) . '/../../PHPSkills/Numerics/GaussianDistribution.php');
use \PHPUnit_Framework_TestCase;
class GaussianDistributionTest extends PHPUnit_Framework_TestCase
{
const ERROR_TOLERANCE = 0.000001;
public function testCumulativeTo()
{
// Verified with WolframAlpha
// (e.g. http://www.wolframalpha.com/input/?i=CDF%5BNormalDistribution%5B0%2C1%5D%2C+0.5%5D )
$this->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);
}
}
?>

@ -0,0 +1,33 @@
<?php
namespace Moserware\Skills;
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]);
}
}
?>

@ -0,0 +1,36 @@
<?php
namespace Moserware\Skills\TrueSkill;
require_once 'PHPUnit/Framework.php';
require_once 'PHPUnit/TextUI/TestRunner.php';
require_once(dirname(__FILE__) . '/../../PHPSkills/TrueSkill/DrawMargin.php');
use \PHPUnit_Framework_TestCase;
class DrawMarginTest extends PHPUnit_Framework_TestCase
{
const ERROR_TOLERANCE = 0.000001;
public function testGetDrawMarginFromDrawProbability()
{
$beta = 25.0 / 6.0;
// The expected values were compared against Ralf Herbrich's implementation in F#
$this->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);
?>

@ -0,0 +1,76 @@
<?php
require_once(dirname(__FILE__) . "/../../PHPSkills/GameInfo.php");
require_once(dirname(__FILE__) . "/../../PHPSkills/Player.php");
require_once(dirname(__FILE__) . "/../../PHPSkills/Team.php");
require_once(dirname(__FILE__) . "/../../PHPSkills/Teams.php");
require_once(dirname(__FILE__) . "/../../PHPSkills/SkillCalculator.php");
use Moserware\Skills\GameInfo;
use Moserware\Skills\Player;
use Moserware\Skills\Team;
use Moserware\Skills\Teams;
use Moserware\Skills\SkillCalculator;
class TrueSkillCalculatorTests
{
const ERROR_TOLERANCE_TRUESKILL = 0.085;
const ERROR_TOLERANCE_MATCH_QUALITY = 0.0005;
// These are the roll-up ones
public static function testAllTwoPlayerScenarios($testClass, SkillCalculator $calculator)
{
self::twoPlayerTestNotDrawn($testClass, $calculator);
//self::twoPlayerTestDrawn($testClass, $calculator);
//self::oneOnOneMassiveUpsetDrawTest($testClass, $calculator);
//self::twoPlayerChessTestNotDrawn($testClass, $calculator);
}
//------------------- Actual Tests ---------------------------
// If you see more than 3 digits of precision in the decimal point, then the expected values calculated from
// F# RalfH's implementation with the same input. It didn't support teams, so team values all came from the
// online calculator at http://atom.research.microsoft.com/trueskill/rankcalculator.aspx
//
// All match quality expected values came from the online calculator
// In both cases, there may be some discrepancy after the first decimal point. I think this is due to my implementation
// using slightly higher precision in GaussianDistribution.
//------------------------------------------------------------------------------
// Two Player Tests
//------------------------------------------------------------------------------
private static function twoPlayerTestNotDrawn($testClass, SkillCalculator $calculator)
{
$player1 = new Player(1);
$player2 = new Player(2);
$gameInfo = new GameInfo();
$team1 = new Team($player1, $gameInfo->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);
}
}
?>

@ -0,0 +1,26 @@
<?php
require_once 'PHPUnit/Framework.php';
require_once 'PHPUnit/TextUI/TestRunner.php';
require_once(dirname(__FILE__) . '/../../PHPSkills/TrueSkill/TwoPlayerTrueSkillCalculator.php');
require_once(dirname(__FILE__) . '/TrueSkillCalculatorTests.php');
use \PHPUnit_Framework_TestCase;
use Moserware\Skills\TrueSkill\TwoPlayerTrueSkillCalculator;
class TwoPlayerTrueSkillCalculatorTest extends PHPUnit_Framework_TestCase
{
public function testTwoPlayerTrueSkillCalculator()
{
$calculator = new TwoPlayerTrueSkillCalculator();
// We only support two players
TrueSkillCalculatorTests::testAllTwoPlayerScenarios($this, $calculator);
}
}
$testSuite = new \PHPUnit_Framework_TestSuite();
$testSuite->addTest( new TwoPlayerTrueSkillCalculatorTest("testTwoPlayerTrueSkillCalculator"));
\PHPUnit_TextUI_TestRunner::run($testSuite);
?>

@ -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);

@ -0,0 +1,5 @@
copy.src.files=false
copy.src.target=
index.file=
run.as=LOCAL
url=http://localhost/

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project-private xmlns="http://www.netbeans.org/ns/project-private/1">
<editor-bookmarks xmlns="http://www.netbeans.org/ns/editor-bookmarks/1"/>
</project-private>

@ -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=.

9
nbproject/project.xml Normal file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://www.netbeans.org/ns/project/1">
<type>org.netbeans.modules.php.project</type>
<configuration>
<data xmlns="http://www.netbeans.org/ns/php-project/1">
<name>phpskills</name>
</data>
</configuration>
</project>

8
test.php Normal file

@ -0,0 +1,8 @@
<?php
require_once(dirname(__FILE__) . "/gaussian.php");
$f = \Moserware\Numerics\GaussianDistribution::cumulativeTo(1.2);
echo $f;
?>

11
web.config Normal file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<handlers>
<remove name="PHP_via_FastCGI" />
<remove name="PHP-ISAPI" />
<remove name="PHP 5" />
<add name="PHP" path="*.php" verb="*" modules="CgiModule" scriptProcessor="C:\Program Files (x86)\PHP\php-cgi.exe" resourceType="Unspecified" preCondition="bitness32" />
</handlers>
</system.webServer>
</configuration>