mirror of
https://github.com/furyfire/trueskill.git
synced 2025-01-15 17:37:39 +00:00
First TwoPlayerTrueSkillCalculator unit test passed
This commit is contained in:
19
PHPSkills/Elo/EloRating.php
Normal file
19
PHPSkills/Elo/EloRating.php
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
41
PHPSkills/Elo/FideEloCalculator.php
Normal file
41
PHPSkills/Elo/FideEloCalculator.php
Normal file
@ -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()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
30
PHPSkills/Elo/FideKFactor.php
Normal file
30
PHPSkills/Elo/FideKFactor.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
?>
|
26
PHPSkills/Elo/GaussianEloCalculator.php
Normal file
26
PHPSkills/Elo/GaussianEloCalculator.php
Normal file
@ -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
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;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
105
PHPSkills/Elo/TwoPlayerEloCalculator.php
Normal file
105
PHPSkills/Elo/TwoPlayerEloCalculator.php
Normal file
@ -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
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
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 . "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
?>
|
16
PHPSkills/ISupportPartialPlay.php
Normal file
16
PHPSkills/ISupportPartialPlay.php
Normal file
@ -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();
|
||||
}
|
||||
|
||||
?>
|
11
PHPSkills/ISupportPartialUpdate.php
Normal file
11
PHPSkills/ISupportPartialUpdate.php
Normal file
@ -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();
|
||||
}
|
||||
?>
|
17
PHPSkills/Numerics/BasicMath.php
Normal file
17
PHPSkills/Numerics/BasicMath.php
Normal file
@ -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;
|
||||
}
|
||||
?>
|
247
PHPSkills/Numerics/GaussianDistribution.php
Normal file
247
PHPSkills/Numerics/GaussianDistribution.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
?>
|
62
PHPSkills/Numerics/Range.php
Normal file
62
PHPSkills/Numerics/Range.php
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
27
PHPSkills/PairwiseComparison.php
Normal file
27
PHPSkills/PairwiseComparison.php
Normal file
@ -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
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();
|
||||
}
|
||||
}
|
||||
?>
|
21
PHPSkills/PlayersRange.php
Normal file
21
PHPSkills/PlayersRange.php
Normal file
@ -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
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
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;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
46
PHPSkills/RatingContainer.php
Normal file
46
PHPSkills/RatingContainer.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
?>
|
81
PHPSkills/SkillCalculator.php
Normal file
81
PHPSkills/SkillCalculator.php
Normal file
@ -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
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
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
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);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
26
PHPSkills/TrueSkill/DrawMargin.php
Normal file
26
PHPSkills/TrueSkill/DrawMargin.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
135
PHPSkills/TrueSkill/TruncatedGaussianCorrectionFunctions.php
Normal file
135
PHPSkills/TrueSkill/TruncatedGaussianCorrectionFunctions.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
?>
|
173
PHPSkills/TrueSkill/TwoPlayerTrueSkillCalculator.php
Normal file
173
PHPSkills/TrueSkill/TwoPlayerTrueSkillCalculator.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
||||
?>
|
46
UnitTests/Elo/EloAssert.php
Normal file
46
UnitTests/Elo/EloAssert.php
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
39
UnitTests/Elo/FideEloCalculatorTest.php
Normal file
39
UnitTests/Elo/FideEloCalculatorTest.php
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
?>
|
||||
|
15
UnitTests/Numerics/BasicMathTest.php
Normal file
15
UnitTests/Numerics/BasicMathTest.php
Normal file
@ -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) );
|
||||
}
|
||||
}
|
||||
?>
|
106
UnitTests/Numerics/GaussianDistributionTest.php
Normal file
106
UnitTests/Numerics/GaussianDistributionTest.php
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
|
33
UnitTests/RankSorterTest.php
Normal file
33
UnitTests/RankSorterTest.php
Normal file
@ -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]);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
36
UnitTests/TrueSkill/DrawMarginTest.php
Normal file
36
UnitTests/TrueSkill/DrawMarginTest.php
Normal file
@ -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);
|
||||
|
||||
?>
|
||||
|
76
UnitTests/TrueSkill/TrueSkillCalculatorTests.php
Normal file
76
UnitTests/TrueSkill/TrueSkillCalculatorTests.php
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
26
UnitTests/TrueSkill/TwoPlayerTrueSkillCalculatorTest.php
Normal file
26
UnitTests/TrueSkill/TwoPlayerTrueSkillCalculatorTest.php
Normal file
@ -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);
|
||||
?>
|
33
UnitTests/runner_example.php
Normal file
33
UnitTests/runner_example.php
Normal file
@ -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);
|
5
nbproject/private/private.properties
Normal file
5
nbproject/private/private.properties
Normal file
@ -0,0 +1,5 @@
|
||||
copy.src.files=false
|
||||
copy.src.target=
|
||||
index.file=
|
||||
run.as=LOCAL
|
||||
url=http://localhost/
|
4
nbproject/private/private.xml
Normal file
4
nbproject/private/private.xml
Normal file
@ -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>
|
7
nbproject/project.properties
Normal file
7
nbproject/project.properties
Normal file
@ -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
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
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
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>
|
Reference in New Issue
Block a user