diff --git a/PHPSkills/Numerics/BasicMath.php b/PHPSkills/Numerics/BasicMath.php
index b487d0e..37f8a1a 100644
--- a/PHPSkills/Numerics/BasicMath.php
+++ b/PHPSkills/Numerics/BasicMath.php
@@ -14,4 +14,11 @@ function square($x)
{
return $x * $x;
}
+
+function sum($itemsToSum, $funcName )
+{
+ $mappedItems = array_map($funcName, $itemsToSum);
+ return array_sum($mappedItems);
+}
+
?>
\ No newline at end of file
diff --git a/PHPSkills/TrueSkill/TruncatedGaussianCorrectionFunctions.php b/PHPSkills/TrueSkill/TruncatedGaussianCorrectionFunctions.php
index 7789b44..2d331ef 100644
--- a/PHPSkills/TrueSkill/TruncatedGaussianCorrectionFunctions.php
+++ b/PHPSkills/TrueSkill/TruncatedGaussianCorrectionFunctions.php
@@ -103,7 +103,7 @@ class TruncatedGaussianCorrectionFunctions
// 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);
+ return self::wWithinMargin($teamPerformanceDifference/$c, $drawMargin/$c);
}
// From F#:
@@ -119,7 +119,7 @@ class TruncatedGaussianCorrectionFunctions
return 1.0;
}
- $vt = vWithinMargin($teamPerformanceDifferenceAbsoluteValue, $drawMargin);
+ $vt = self::vWithinMargin($teamPerformanceDifferenceAbsoluteValue, $drawMargin);
return $vt*$vt +
(
diff --git a/PHPSkills/TrueSkill/TwoPlayerTrueSkillCalculator.php b/PHPSkills/TrueSkill/TwoPlayerTrueSkillCalculator.php
index 34928b3..dea523c 100644
--- a/PHPSkills/TrueSkill/TwoPlayerTrueSkillCalculator.php
+++ b/PHPSkills/TrueSkill/TwoPlayerTrueSkillCalculator.php
@@ -2,6 +2,7 @@
namespace Moserware\Skills\TrueSkill;
+require_once(dirname(__FILE__) . "/../Guard.php");
require_once(dirname(__FILE__) . "/../PairwiseComparison.php");
require_once(dirname(__FILE__) . "/../RankSorter.php");
require_once(dirname(__FILE__) . "/../Rating.php");
@@ -16,6 +17,7 @@ require_once(dirname(__FILE__) . "/../Numerics/BasicMath.php");
require_once(dirname(__FILE__) . "/DrawMargin.php");
require_once(dirname(__FILE__) . "/TruncatedGaussianCorrectionFunctions.php");
+use Moserware\Skills\Guard;
use Moserware\Skills\PairwiseComparison;
use Moserware\Skills\RankSorter;
use Moserware\Skills\Rating;
@@ -44,7 +46,8 @@ class TwoPlayerTrueSkillCalculator extends SkillCalculator
array $teams,
array $teamRanks)
{
- // Basic argument checking
+ // Basic argument checking
+ Guard::argumentNotNull($gameInfo, "gameInfo");
$this->validateTeamCountAndPlayersCountPerTeam($teams);
// Make sure things are in order
@@ -136,7 +139,8 @@ class TwoPlayerTrueSkillCalculator extends SkillCalculator
///
public function calculateMatchQuality($gameInfo, array $teams)
- {
+ {
+ Guard::argumentNotNull($gameInfo, "gameInfo");
$this->validateTeamCountAndPlayersCountPerTeam($teams);
$team1 = $teams[0];
diff --git a/PHPSkills/TrueSkill/TwoTeamTrueSkillCalculator.php b/PHPSkills/TrueSkill/TwoTeamTrueSkillCalculator.php
new file mode 100644
index 0000000..5ce33e4
--- /dev/null
+++ b/PHPSkills/TrueSkill/TwoTeamTrueSkillCalculator.php
@@ -0,0 +1,222 @@
+
+/// Calculates new ratings for only two teams where each team has 1 or more players.
+///
+///
+/// When you only have two teams, the math is still simple: no factor graphs are used yet.
+///
+class TwoTeamTrueSkillCalculator extends SkillCalculator
+{
+ public function __construct()
+ {
+ parent::__construct(SkillCalculatorSupportedOptions::NONE, TeamsRange::exactly(2), PlayersRange::atLeast(1));
+ }
+
+ public function calculateNewRatings($gameInfo,
+ array $teams,
+ array $teamRanks)
+ {
+ Guard::argumentNotNull($gameInfo, "gameInfo");
+ $this->validateTeamCountAndPlayersCountPerTeam($teams);
+
+ RankSorter::sort($teams, $teamRanks);
+
+ $team1 = $teams[0];
+ $team2 = $teams[1];
+
+ $wasDraw = ($teamRanks[0] == $teamRanks[1]);
+
+ $results = new RatingContainer();
+
+ self::updatePlayerRatings($gameInfo,
+ $results,
+ $team1,
+ $team2,
+ $wasDraw ? PairwiseComparison::DRAW : PairwiseComparison::WIN);
+
+ self::updatePlayerRatings($gameInfo,
+ $results,
+ $team2,
+ $team1,
+ $wasDraw ? PairwiseComparison::DRAW : PairwiseComparison::LOSE);
+
+ return $results;
+ }
+
+ private static function updatePlayerRatings(GameInfo $gameInfo,
+ RatingContainer &$newPlayerRatings,
+ Team $selfTeam,
+ Team $otherTeam,
+ $selfToOtherTeamComparison)
+ {
+ $drawMargin = DrawMargin::getDrawMarginFromDrawProbability($gameInfo->getDrawProbability(),
+ $gameInfo->getBeta());
+
+ $betaSquared = square($gameInfo->getBeta());
+ $tauSquared = square($gameInfo->getDynamicsFactor());
+
+ $totalPlayers = $selfTeam->count() + $otherTeam->count();
+
+ $meanGetter =
+ function($currentRating)
+ {
+ return $currentRating->getMean();
+ };
+
+ $selfMeanSum = sum($selfTeam->getAllRatings(), $meanGetter);
+ $otherTeamMeanSum = sum($otherTeam->getAllRatings(), $meanGetter);
+
+ $varianceGetter =
+ function($currentRating)
+ {
+ return square($currentRating->getStandardDeviation());
+ };
+
+ $c = sqrt(
+ sum($selfTeam->getAllRatings(), $varianceGetter)
+ +
+ sum($otherTeam->getAllRatings(), $varianceGetter)
+ +
+ $totalPlayers*$betaSquared);
+
+ $winningMean = $selfMeanSum;
+ $losingMean = $otherTeamMeanSum;
+
+ switch ($selfToOtherTeamComparison)
+ {
+ case PairwiseComparison::WIN:
+ case PairwiseComparison::DRAW:
+ // NOP
+ break;
+ case PairwiseComparison::LOSE:
+ $winningMean = $otherTeamMeanSum;
+ $losingMean = $selfMeanSum;
+ break;
+ }
+
+ $meanDelta = $winningMean - $losingMean;
+
+ if ($selfToOtherTeamComparison != PairwiseComparison::DRAW)
+ {
+ // non-draw case
+ $v = TruncatedGaussianCorrectionFunctions::vExceedsMarginScaled($meanDelta, $drawMargin, $c);
+ $w = TruncatedGaussianCorrectionFunctions::wExceedsMarginScaled($meanDelta, $drawMargin, $c);
+ $rankMultiplier = (int) $selfToOtherTeamComparison;
+ }
+ else
+ {
+ // assume draw
+ $v = TruncatedGaussianCorrectionFunctions::vWithinMarginScaled($meanDelta, $drawMargin, $c);
+ $w = TruncatedGaussianCorrectionFunctions::wWithinMarginScaled($meanDelta, $drawMargin, $c);
+ $rankMultiplier = 1;
+ }
+
+ foreach ($selfTeam->getAllPlayers() as $selfTeamCurrentPlayer)
+ {
+ $previousPlayerRating = $selfTeam->getRating($selfTeamCurrentPlayer);
+
+ $meanMultiplier = (square($previousPlayerRating->getStandardDeviation()) + $tauSquared)/$c;
+ $stdDevMultiplier = (square($previousPlayerRating->getStandardDeviation()) + $tauSquared)/square($c);
+
+ $playerMeanDelta = ($rankMultiplier*$meanMultiplier*$v);
+ $newMean = $previousPlayerRating->getMean() + $playerMeanDelta;
+
+ $newStdDev =
+ sqrt((square($previousPlayerRating->getStandardDeviation()) + $tauSquared)*(1 - $w*$stdDevMultiplier));
+
+ $newPlayerRatings->setRating($selfTeamCurrentPlayer, new Rating($newMean, $newStdDev));
+ }
+ }
+
+ ///
+ public function calculateMatchQuality($gameInfo, array $teams)
+ {
+ Guard::argumentNotNull($gameInfo, "gameInfo");
+ $this->validateTeamCountAndPlayersCountPerTeam($teams);
+
+ // We've verified that there's just two teams
+ $team1Ratings = $teams[0]->getAllRatings();
+ $team1Count = count($team1Ratings);
+
+ $team2Ratings = $teams[1]->getAllRatings();
+ $team2Count = count($team2Ratings);
+
+ $totalPlayers = $team1Count + $team2Count;
+
+ $betaSquared = square($gameInfo->getBeta());
+
+ $meanGetter =
+ function($currentRating)
+ {
+ return $currentRating->getMean();
+ };
+
+ $varianceGetter =
+ function($currentRating)
+ {
+ return square($currentRating->getStandardDeviation());
+ };
+
+ $team1MeanSum = sum($team1Ratings, $meanGetter);
+ $team1StdDevSquared = sum($team1Ratings, $varianceGetter);
+
+ $team2MeanSum = sum($team2Ratings, $meanGetter);
+ $team2SigmaSquared = sum($team2Ratings, $varianceGetter);
+
+ // This comes from equation 4.1 in the TrueSkill paper on page 8
+ // The equation was broken up into the part under the square root sign and
+ // the exponential part to make the code easier to read.
+
+ $sqrtPart
+ = sqrt(
+ ($totalPlayers*$betaSquared)
+ /
+ ($totalPlayers*$betaSquared + $team1StdDevSquared + $team2SigmaSquared)
+ );
+
+ $expPart
+ = exp(
+ (-1*square($team1MeanSum - $team2MeanSum))
+ /
+ (2*($totalPlayers*$betaSquared + $team1StdDevSquared + $team2SigmaSquared))
+ );
+
+ return $expPart*$sqrtPart;
+ }
+}
+?>
diff --git a/UnitTests/TrueSkill/TrueSkillCalculatorTests.php b/UnitTests/TrueSkill/TrueSkillCalculatorTests.php
index d420ddd..b1cffa1 100644
--- a/UnitTests/TrueSkill/TrueSkillCalculatorTests.php
+++ b/UnitTests/TrueSkill/TrueSkillCalculatorTests.php
@@ -1,12 +1,14 @@
calculateMatchQuality($gameInfo, $teams));
}
+ private static function twoPlayerTestDrawn($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, 1));
+
+ $player1NewRating = $newRatings->getRating($player1);
+ self::assertRating($testClass, 25.0, 6.4575196623173081, $player1NewRating);
+
+ $player2NewRating = $newRatings->getRating($player2);
+ self::assertRating($testClass, 25.0, 6.4575196623173081, $player2NewRating);
+
+ self::assertMatchQuality($testClass, 0.447, $calculator->calculateMatchQuality($gameInfo, $teams));
+ }
+
+ private static function twoPlayerChessTestNotDrawn($testClass, SkillCalculator $calculator)
+ {
+ // Inspired by a real bug :-)
+ $player1 = new Player(1);
+ $player2 = new Player(2);
+ $gameInfo = new GameInfo(1200.0, 1200.0 / 3.0, 200.0, 1200.0 / 300.0, 0.03);
+
+ $team1 = new Team($player1, new Rating(1301.0007, 42.9232));
+ $team2 = new Team($player2, new Rating(1188.7560, 42.5570));
+
+ $newRatings = $calculator->calculateNewRatings($gameInfo, Teams::concat($team1, $team2), array(1, 2));
+
+ $player1NewRating = $newRatings->getRating($player1);
+ self::assertRating($testClass, 1304.7820836053318, 42.843513887848658, $player1NewRating);
+
+ $player2NewRating = $newRatings->getRating($player2);
+ self::assertRating($testClass, 1185.0383099003536, 42.485604606897752, $player2NewRating);
+ }
+
+ private static function oneOnOneMassiveUpsetDrawTest($testClass, SkillCalculator $calculator)
+ {
+ $player1 = new Player(1);
+
+ $gameInfo = new GameInfo();
+
+ $team1 = new Team($player1, $gameInfo->getDefaultRating());
+
+ $player2 = new Player(2);
+
+ $team2 = new Team($player2, new Rating(50, 12.5));
+
+ $teams = Teams::concat($team1, $team2);
+
+ $newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, array(1, 1));
+
+ // Winners
+ self::assertRating($testClass, 31.662, 7.137, $newRatingsWinLose->getRating($player1));
+
+ // Losers
+ self::assertRating($testClass, 35.010, 7.910, $newRatingsWinLose->getRating($player2));
+
+ self::assertMatchQuality($testClass, 0.110, $calculator->calculateMatchQuality($gameInfo, $teams));
+ }
+
+ //------------------------------------------------------------------------------
+ // Two Team Tests
+ //------------------------------------------------------------------------------
+
+ private static function twoOnTwoSimpleTest($testClass, SkillCalculator $calculator)
+ {
+ $player1 = new Player(1);
+ $player2 = new Player(2);
+
+ $gameInfo = new GameInfo();
+
+ $team1 = new Team();
+ $team1->addPlayer($player1, $gameInfo->getDefaultRating());
+ $team1->addPlayer($player2, $gameInfo->getDefaultRating());
+
+ $player3 = new Player(3);
+ $player4 = new Player(4);
+
+ $team2 = new Team();
+ $team2->addPlayer($player3, $gameInfo->getDefaultRating());
+ $team2->addPlayer($player4, $gameInfo->getDefaultRating());
+
+ $teams = Teams::concat($team1, $team2);
+ $newRatingsWinLose = $calculator->calculateNewRatings($gameInfo, $teams, array(1, 2));
+
+ // Winners
+ self::assertRating($testClass, 28.108, 7.774, $newRatingsWinLose->getRating($player1));
+ self::assertRating($testClass, 28.108, 7.774, $newRatingsWinLose->getRating($player2));
+
+ // Losers
+ self::assertRating($testClass, 21.892, 7.774, $newRatingsWinLose->getRating($player3));
+ self::assertRating($testClass, 21.892, 7.774, $newRatingsWinLose->getRating($player4));
+
+ 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);
diff --git a/UnitTests/TrueSkill/TwoTeamTrueSkillCalculatorTest.php b/UnitTests/TrueSkill/TwoTeamTrueSkillCalculatorTest.php
new file mode 100644
index 0000000..cd9aac7
--- /dev/null
+++ b/UnitTests/TrueSkill/TwoTeamTrueSkillCalculatorTest.php
@@ -0,0 +1,27 @@
+addTest( new TwoTeamTrueSkillCalculatorTest("testTwoTeamTrueSkillCalculator"));
+
+\PHPUnit_TextUI_TestRunner::run($testSuite);
+?>