First TwoPlayerTrueSkillCalculator unit test passed

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

View 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;
}
}
?>

View 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;
}
}
?>

View 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;
}
}
?>