Files
trueskill/src/TrueSkill/TwoTeamTrueSkillCalculator.php

195 lines
6.6 KiB
PHP
Raw Normal View History

2022-07-05 15:55:47 +02:00
<?php
2024-02-02 14:53:38 +00:00
declare(strict_types=1);
2022-07-05 15:55:47 +02:00
namespace DNW\Skills\TrueSkill;
2022-07-05 15:33:34 +02:00
use DNW\Skills\GameInfo;
use DNW\Skills\Guard;
use DNW\Skills\Numerics\BasicMath;
use DNW\Skills\PairwiseComparison;
2022-07-05 15:55:47 +02:00
use DNW\Skills\PlayersRange;
2022-07-05 15:33:34 +02:00
use DNW\Skills\RankSorter;
use DNW\Skills\Rating;
use DNW\Skills\RatingContainer;
use DNW\Skills\SkillCalculator;
use DNW\Skills\Team;
2022-07-05 15:55:47 +02:00
use DNW\Skills\TeamsRange;
/**
* 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()
{
2024-05-21 12:24:59 +00:00
parent::__construct(SkillCalculator::NONE, TeamsRange::exactly(2), PlayersRange::atLeast(1));
}
2024-02-21 13:48:37 +00:00
2023-08-02 14:24:19 +00:00
/**
* {@inheritdoc}
*/
2025-01-28 09:20:03 +00:00
#[\Override]
2023-08-01 11:26:38 +00:00
public function calculateNewRatings(GameInfo $gameInfo, array $teams, array $teamRanks): RatingContainer
{
$this->validateTeamCountAndPlayersCountPerTeam($teams);
RankSorter::sort($teams, $teamRanks);
$team1 = $teams[0];
$team2 = $teams[1];
$wasDraw = ($teamRanks[0] == $teamRanks[1]);
$results = new RatingContainer();
2023-08-01 13:35:44 +00:00
self::updatePlayerRatings(
$gameInfo,
$results,
$team1,
$team2,
2023-08-01 13:35:44 +00:00
$wasDraw ? PairwiseComparison::DRAW : PairwiseComparison::WIN
);
2023-08-01 13:35:44 +00:00
self::updatePlayerRatings(
$gameInfo,
$results,
$team2,
$team1,
2023-08-01 13:35:44 +00:00
$wasDraw ? PairwiseComparison::DRAW : PairwiseComparison::LOSE
);
return $results;
}
2023-08-01 13:53:19 +00:00
private static function updatePlayerRatings(
GameInfo $gameInfo,
RatingContainer $newPlayerRatings,
Team $selfTeam,
Team $otherTeam,
PairwiseComparison $selfToOtherTeamComparison
): void
{
$drawMargin = DrawMargin::getDrawMarginFromDrawProbability(
$gameInfo->getDrawProbability(),
$gameInfo->getBeta()
);
2016-05-24 15:12:29 +02:00
$betaSquared = BasicMath::square($gameInfo->getBeta());
$tauSquared = BasicMath::square($gameInfo->getDynamicsFactor());
$totalPlayers = $selfTeam->count() + $otherTeam->count();
2024-02-21 13:48:37 +00:00
$meanGetter = static fn(Rating $currentRating): float => $currentRating->getMean();
2016-05-24 15:12:29 +02:00
$selfMeanSum = BasicMath::sum($selfTeam->getAllRatings(), $meanGetter);
$otherTeamMeanSum = BasicMath::sum($otherTeam->getAllRatings(), $meanGetter);
2024-02-21 13:48:37 +00:00
$varianceGetter = static fn(Rating $currentRating): float => BasicMath::square($currentRating->getStandardDeviation());
$c = sqrt(
2016-05-24 15:12:29 +02:00
BasicMath::sum($selfTeam->getAllRatings(), $varianceGetter)
+
2016-05-24 15:12:29 +02:00
BasicMath::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);
2023-08-01 11:26:38 +00:00
$rankMultiplier = $selfToOtherTeamComparison->value;
} else {
// assume draw
$v = TruncatedGaussianCorrectionFunctions::vWithinMarginScaled($meanDelta, $drawMargin, $c);
$w = TruncatedGaussianCorrectionFunctions::wWithinMarginScaled($meanDelta, $drawMargin, $c);
$rankMultiplier = 1;
}
$selfTeamAllPlayers = $selfTeam->getAllPlayers();
2024-07-04 09:38:03 +00:00
foreach ($selfTeamAllPlayers as $selfTeamCurPlayer) {
$localSelfTeamCurPlayer = $selfTeamCurPlayer;
$previousPlayerRating = $selfTeam->getRating($localSelfTeamCurPlayer);
2016-05-24 15:12:29 +02:00
$meanMultiplier = (BasicMath::square($previousPlayerRating->getStandardDeviation()) + $tauSquared) / $c;
$stdDevMultiplier = (BasicMath::square($previousPlayerRating->getStandardDeviation()) + $tauSquared) / BasicMath::square($c);
$playerMeanDelta = ($rankMultiplier * $meanMultiplier * $v);
$newMean = $previousPlayerRating->getMean() + $playerMeanDelta;
2016-05-24 15:12:29 +02:00
$newStdDev = sqrt(
(BasicMath::square($previousPlayerRating->getStandardDeviation()) + $tauSquared) * (1 - $w * $stdDevMultiplier)
);
2024-07-04 09:38:03 +00:00
$newPlayerRatings->setRating($localSelfTeamCurPlayer, new Rating($newMean, $newStdDev));
}
}
/**
2016-05-24 15:12:29 +02:00
* {@inheritdoc}
*/
2025-01-28 09:20:03 +00:00
#[\Override]
2023-08-01 11:26:38 +00:00
public function calculateMatchQuality(GameInfo $gameInfo, array $teams): float
{
$this->validateTeamCountAndPlayersCountPerTeam($teams);
// We've verified that there's just two teams
$team1Ratings = $teams[0]->getAllRatings();
2023-08-02 14:24:19 +00:00
$team1Count = count($team1Ratings);
$team2Ratings = $teams[1]->getAllRatings();
2023-08-02 14:24:19 +00:00
$team2Count = count($team2Ratings);
$totalPlayers = $team1Count + $team2Count;
2016-05-24 15:12:29 +02:00
$betaSquared = BasicMath::square($gameInfo->getBeta());
2024-02-21 13:48:37 +00:00
$meanGetter = static fn(Rating $currentRating): float => $currentRating->getMean();
2024-02-21 13:48:37 +00:00
$varianceGetter = static fn(Rating $currentRating): float => BasicMath::square($currentRating->getStandardDeviation());
2016-05-24 15:12:29 +02:00
$team1MeanSum = BasicMath::sum($team1Ratings, $meanGetter);
$team1StdDevSquared = BasicMath::sum($team1Ratings, $varianceGetter);
2016-05-24 15:12:29 +02:00
$team2MeanSum = BasicMath::sum($team2Ratings, $meanGetter);
$team2StdDevSquared = BasicMath::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 + $team2StdDevSquared)
);
$expPart = exp(
2016-05-24 15:12:29 +02:00
(-1 * BasicMath::square($team1MeanSum - $team2MeanSum))
/
(2 * ($totalPlayers * $betaSquared + $team1StdDevSquared + $team2StdDevSquared))
);
return $expPart * $sqrtPart;
}
2022-07-05 15:55:47 +02:00
}