mirror of
https://github.com/furyfire/trueskill.git
synced 2025-01-16 01:47: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