2022-07-05 15:55:47 +02:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace DNW\Skills\TrueSkill;
|
2010-09-18 21:19:51 -04:00
|
|
|
|
2022-07-05 15:33:34 +02:00
|
|
|
use DNW\Skills\GameInfo;
|
|
|
|
use DNW\Skills\Guard;
|
|
|
|
use DNW\Skills\Numerics\BasicMath;
|
|
|
|
use DNW\Skills\Numerics\DiagonalMatrix;
|
|
|
|
use DNW\Skills\Numerics\Matrix;
|
|
|
|
use DNW\Skills\Numerics\Vector;
|
|
|
|
use DNW\Skills\PartialPlay;
|
|
|
|
use DNW\Skills\PlayersRange;
|
|
|
|
use DNW\Skills\RankSorter;
|
|
|
|
use DNW\Skills\SkillCalculator;
|
|
|
|
use DNW\Skills\SkillCalculatorSupportedOptions;
|
|
|
|
use DNW\Skills\TeamsRange;
|
2010-09-18 21:19:51 -04:00
|
|
|
|
2010-10-08 21:44:36 -04:00
|
|
|
/**
|
|
|
|
* Calculates TrueSkill using a full factor graph.
|
|
|
|
*/
|
2010-09-18 11:11:44 -04:00
|
|
|
class FactorGraphTrueSkillCalculator extends SkillCalculator
|
|
|
|
{
|
|
|
|
public function __construct()
|
|
|
|
{
|
|
|
|
parent::__construct(SkillCalculatorSupportedOptions::PARTIAL_PLAY | SkillCalculatorSupportedOptions::PARTIAL_UPDATE, TeamsRange::atLeast(2), PlayersRange::atLeast(1));
|
|
|
|
}
|
|
|
|
|
2016-05-24 15:12:29 +02:00
|
|
|
public function calculateNewRatings(GameInfo $gameInfo,
|
2010-09-18 11:11:44 -04:00
|
|
|
array $teams,
|
|
|
|
array $teamRanks)
|
|
|
|
{
|
2022-07-05 15:55:47 +02:00
|
|
|
Guard::argumentNotNull($gameInfo, 'gameInfo');
|
2010-09-18 11:11:44 -04:00
|
|
|
$this->validateTeamCountAndPlayersCountPerTeam($teams);
|
|
|
|
|
|
|
|
RankSorter::sort($teams, $teamRanks);
|
|
|
|
|
|
|
|
$factorGraph = new TrueSkillFactorGraph($gameInfo, $teams, $teamRanks);
|
|
|
|
$factorGraph->buildGraph();
|
|
|
|
$factorGraph->runSchedule();
|
2016-05-24 14:10:39 +02:00
|
|
|
|
2022-07-05 16:21:06 +02:00
|
|
|
$factorGraph->getProbabilityOfRanking();
|
2010-09-18 11:11:44 -04:00
|
|
|
|
|
|
|
return $factorGraph->getUpdatedRatings();
|
|
|
|
}
|
|
|
|
|
2016-05-24 16:31:21 +02:00
|
|
|
public function calculateMatchQuality(GameInfo $gameInfo, array $teams)
|
2010-09-18 11:11:44 -04:00
|
|
|
{
|
|
|
|
// We need to create the A matrix which is the player team assigments.
|
2010-09-18 21:19:51 -04:00
|
|
|
$teamAssignmentsList = $teams;
|
2022-07-05 16:21:06 +02:00
|
|
|
$skillsMatrix = self::getPlayerCovarianceMatrix($teamAssignmentsList);
|
|
|
|
$meanVector = self::getPlayerMeansVector($teamAssignmentsList);
|
2010-09-18 11:11:44 -04:00
|
|
|
$meanVectorTranspose = $meanVector->getTranspose();
|
|
|
|
|
2022-07-05 16:21:06 +02:00
|
|
|
$playerTeamAssignmentsMatrix = self::createPlayerTeamAssignmentMatrix($teamAssignmentsList, $meanVector->getRowCount());
|
2010-09-18 11:11:44 -04:00
|
|
|
$playerTeamAssignmentsMatrixTranspose = $playerTeamAssignmentsMatrix->getTranspose();
|
|
|
|
|
2016-05-24 15:12:29 +02:00
|
|
|
$betaSquared = BasicMath::square($gameInfo->getBeta());
|
2010-09-18 11:11:44 -04:00
|
|
|
|
|
|
|
$start = Matrix::multiply($meanVectorTranspose, $playerTeamAssignmentsMatrix);
|
2016-05-24 14:10:39 +02:00
|
|
|
|
2010-09-18 11:11:44 -04:00
|
|
|
$aTa = Matrix::multiply(
|
2016-05-24 14:10:39 +02:00
|
|
|
Matrix::scalarMultiply($betaSquared, $playerTeamAssignmentsMatrixTranspose),
|
|
|
|
$playerTeamAssignmentsMatrix
|
|
|
|
);
|
2010-09-18 11:11:44 -04:00
|
|
|
|
|
|
|
$aTSA = Matrix::multiply(
|
2016-05-24 14:10:39 +02:00
|
|
|
Matrix::multiply($playerTeamAssignmentsMatrixTranspose, $skillsMatrix),
|
|
|
|
$playerTeamAssignmentsMatrix
|
|
|
|
);
|
2010-09-18 11:11:44 -04:00
|
|
|
|
|
|
|
$middle = Matrix::add($aTa, $aTSA);
|
|
|
|
|
|
|
|
$middleInverse = $middle->getInverse();
|
|
|
|
|
|
|
|
$end = Matrix::multiply($playerTeamAssignmentsMatrixTranspose, $meanVector);
|
|
|
|
|
|
|
|
$expPartMatrix = Matrix::scalarMultiply(-0.5, (Matrix::multiply(Matrix::multiply($start, $middleInverse), $end)));
|
|
|
|
$expPart = $expPartMatrix->getDeterminant();
|
|
|
|
|
|
|
|
$sqrtPartNumerator = $aTa->getDeterminant();
|
|
|
|
$sqrtPartDenominator = $middle->getDeterminant();
|
|
|
|
$sqrtPart = $sqrtPartNumerator / $sqrtPartDenominator;
|
|
|
|
|
2022-07-05 16:21:06 +02:00
|
|
|
return exp($expPart) * sqrt($sqrtPart);
|
2010-09-18 11:11:44 -04:00
|
|
|
}
|
|
|
|
|
2016-05-24 16:31:21 +02:00
|
|
|
private static function getPlayerMeansVector(array $teamAssignmentsList)
|
2010-09-18 11:11:44 -04:00
|
|
|
{
|
|
|
|
// A simple vector of all the player means.
|
2010-10-02 23:22:01 -04:00
|
|
|
return new Vector(self::getPlayerRatingValues($teamAssignmentsList,
|
2022-07-05 16:21:06 +02:00
|
|
|
fn($rating) => $rating->getMean()));
|
2010-09-18 11:11:44 -04:00
|
|
|
}
|
|
|
|
|
2016-05-24 16:31:21 +02:00
|
|
|
private static function getPlayerCovarianceMatrix(array $teamAssignmentsList)
|
2010-09-18 11:11:44 -04:00
|
|
|
{
|
|
|
|
// This is a square matrix whose diagonal values represent the variance (square of standard deviation) of all
|
|
|
|
// players.
|
|
|
|
return new DiagonalMatrix(
|
2016-05-24 14:10:39 +02:00
|
|
|
self::getPlayerRatingValues($teamAssignmentsList,
|
2022-07-05 16:21:06 +02:00
|
|
|
fn($rating) => BasicMath::square($rating->getStandardDeviation())));
|
2010-09-18 11:11:44 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Helper function that gets a list of values for all player ratings
|
2016-05-24 16:31:21 +02:00
|
|
|
private static function getPlayerRatingValues(array $teamAssignmentsList, $playerRatingFunction)
|
2010-09-18 11:11:44 -04:00
|
|
|
{
|
2022-07-05 15:55:47 +02:00
|
|
|
$playerRatingValues = [];
|
2010-09-18 11:11:44 -04:00
|
|
|
|
2016-05-24 14:10:39 +02:00
|
|
|
foreach ($teamAssignmentsList as $currentTeam) {
|
|
|
|
foreach ($currentTeam->getAllRatings() as $currentRating) {
|
2010-09-18 11:11:44 -04:00
|
|
|
$playerRatingValues[] = $playerRatingFunction($currentRating);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $playerRatingValues;
|
|
|
|
}
|
|
|
|
|
2016-05-24 16:31:21 +02:00
|
|
|
private static function createPlayerTeamAssignmentMatrix($teamAssignmentsList, $totalPlayers)
|
2010-09-18 11:11:44 -04:00
|
|
|
{
|
|
|
|
// The team assignment matrix is often referred to as the "A" matrix. It's a matrix whose rows represent the players
|
|
|
|
// and the columns represent teams. At Matrix[row, column] represents that player[row] is on team[col]
|
|
|
|
// Positive values represent an assignment and a negative value means that we subtract the value of the next
|
|
|
|
// team since we're dealing with pairs. This means that this matrix always has teams - 1 columns.
|
|
|
|
// The only other tricky thing is that values represent the play percentage.
|
|
|
|
|
|
|
|
// For example, consider a 3 team game where team1 is just player1, team 2 is player 2 and player 3, and
|
|
|
|
// team3 is just player 4. Furthermore, player 2 and player 3 on team 2 played 25% and 75% of the time
|
|
|
|
// (e.g. partial play), the A matrix would be:
|
|
|
|
|
|
|
|
// A = this 4x2 matrix:
|
|
|
|
// | 1.00 0.00 |
|
|
|
|
// | -0.25 0.25 |
|
|
|
|
// | -0.75 0.75 |
|
|
|
|
// | 0.00 -1.00 |
|
|
|
|
|
2022-07-05 15:55:47 +02:00
|
|
|
$playerAssignments = [];
|
2010-09-18 11:11:44 -04:00
|
|
|
$totalPreviousPlayers = 0;
|
|
|
|
|
2022-07-05 16:21:06 +02:00
|
|
|
$teamAssignmentsListCount = is_countable($teamAssignmentsList) ? count($teamAssignmentsList) : 0;
|
2010-09-18 11:11:44 -04:00
|
|
|
|
2010-10-03 17:42:31 -04:00
|
|
|
$currentColumn = 0;
|
|
|
|
|
2016-05-24 14:10:39 +02:00
|
|
|
for ($i = 0; $i < $teamAssignmentsListCount - 1; $i++) {
|
2010-09-18 11:11:44 -04:00
|
|
|
$currentTeam = $teamAssignmentsList[$i];
|
|
|
|
|
|
|
|
// Need to add in 0's for all the previous players, since they're not
|
|
|
|
// on this team
|
2022-07-05 15:55:47 +02:00
|
|
|
$playerAssignments[$currentColumn] = ($totalPreviousPlayers > 0) ? \array_fill(0, $totalPreviousPlayers, 0) : [];
|
2010-09-18 11:11:44 -04:00
|
|
|
|
2016-05-24 14:10:39 +02:00
|
|
|
foreach ($currentTeam->getAllPlayers() as $currentPlayer) {
|
2010-10-03 17:42:31 -04:00
|
|
|
$playerAssignments[$currentColumn][] = PartialPlay::getPartialPlayPercentage($currentPlayer);
|
2010-09-18 11:11:44 -04:00
|
|
|
// indicates the player is on the team
|
|
|
|
$totalPreviousPlayers++;
|
|
|
|
}
|
|
|
|
|
2010-10-03 17:42:31 -04:00
|
|
|
$rowsRemaining = $totalPlayers - $totalPreviousPlayers;
|
|
|
|
|
2010-09-18 11:11:44 -04:00
|
|
|
$nextTeam = $teamAssignmentsList[$i + 1];
|
2016-05-24 14:10:39 +02:00
|
|
|
foreach ($nextTeam->getAllPlayers() as $nextTeamPlayer) {
|
2010-09-18 11:11:44 -04:00
|
|
|
// Add a -1 * playing time to represent the difference
|
2010-10-03 17:42:31 -04:00
|
|
|
$playerAssignments[$currentColumn][] = -1 * PartialPlay::getPartialPlayPercentage($nextTeamPlayer);
|
|
|
|
$rowsRemaining--;
|
|
|
|
}
|
|
|
|
|
2016-05-24 14:10:39 +02:00
|
|
|
for ($ixAdditionalRow = 0; $ixAdditionalRow < $rowsRemaining; $ixAdditionalRow++) {
|
2010-10-03 17:42:31 -04:00
|
|
|
// Pad with zeros
|
|
|
|
$playerAssignments[$currentColumn][] = 0;
|
2010-09-18 11:11:44 -04:00
|
|
|
}
|
2010-10-02 23:22:01 -04:00
|
|
|
|
2010-10-03 17:42:31 -04:00
|
|
|
$currentColumn++;
|
2010-09-18 11:11:44 -04:00
|
|
|
}
|
|
|
|
|
2022-07-05 16:21:06 +02:00
|
|
|
return Matrix::fromColumnValues($totalPlayers, $teamAssignmentsListCount - 1, $playerAssignments);
|
2010-09-18 11:11:44 -04:00
|
|
|
}
|
2022-07-05 15:55:47 +02:00
|
|
|
}
|