trueskill/src/TrueSkill/FactorGraphTrueSkillCalculator.php

176 lines
6.9 KiB
PHP
Raw Normal View History

2022-07-05 15:55:47 +02:00
<?php
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\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;
2023-08-01 12:13:24 +00:00
use DNW\Skills\RatingContainer;
/**
* Calculates TrueSkill using a full factor graph.
*/
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,
array $teams,
2023-08-01 12:13:24 +00:00
array $teamRanks): RatingContainer
{
2022-07-05 15:55:47 +02:00
Guard::argumentNotNull($gameInfo, 'gameInfo');
$this->validateTeamCountAndPlayersCountPerTeam($teams);
RankSorter::sort($teams, $teamRanks);
$factorGraph = new TrueSkillFactorGraph($gameInfo, $teams, $teamRanks);
$factorGraph->buildGraph();
$factorGraph->runSchedule();
2022-07-05 16:21:06 +02:00
$factorGraph->getProbabilityOfRanking();
return $factorGraph->getUpdatedRatings();
}
2023-08-01 12:43:58 +00:00
public function calculateMatchQuality(GameInfo $gameInfo, array $teams): float
{
// We need to create the A matrix which is the player team assigments.
$teamAssignmentsList = $teams;
2022-07-05 16:21:06 +02:00
$skillsMatrix = self::getPlayerCovarianceMatrix($teamAssignmentsList);
$meanVector = self::getPlayerMeansVector($teamAssignmentsList);
$meanVectorTranspose = $meanVector->getTranspose();
2022-07-05 16:21:06 +02:00
$playerTeamAssignmentsMatrix = self::createPlayerTeamAssignmentMatrix($teamAssignmentsList, $meanVector->getRowCount());
$playerTeamAssignmentsMatrixTranspose = $playerTeamAssignmentsMatrix->getTranspose();
2016-05-24 15:12:29 +02:00
$betaSquared = BasicMath::square($gameInfo->getBeta());
$start = Matrix::multiply($meanVectorTranspose, $playerTeamAssignmentsMatrix);
$aTa = Matrix::multiply(
Matrix::scalarMultiply($betaSquared, $playerTeamAssignmentsMatrixTranspose),
$playerTeamAssignmentsMatrix
);
$aTSA = Matrix::multiply(
Matrix::multiply($playerTeamAssignmentsMatrixTranspose, $skillsMatrix),
$playerTeamAssignmentsMatrix
);
$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);
}
private static function getPlayerMeansVector(array $teamAssignmentsList)
{
// A simple vector of all the player means.
return new Vector(self::getPlayerRatingValues($teamAssignmentsList,
2022-07-05 16:32:18 +02:00
fn ($rating) => $rating->getMean()));
}
private static function getPlayerCovarianceMatrix(array $teamAssignmentsList)
{
// This is a square matrix whose diagonal values represent the variance (square of standard deviation) of all
// players.
return new DiagonalMatrix(
self::getPlayerRatingValues($teamAssignmentsList,
2022-07-05 16:32:18 +02:00
fn ($rating) => BasicMath::square($rating->getStandardDeviation())));
}
// Helper function that gets a list of values for all player ratings
private static function getPlayerRatingValues(array $teamAssignmentsList, $playerRatingFunction)
{
2022-07-05 15:55:47 +02:00
$playerRatingValues = [];
foreach ($teamAssignmentsList as $currentTeam) {
foreach ($currentTeam->getAllRatings() as $currentRating) {
$playerRatingValues[] = $playerRatingFunction($currentRating);
}
}
return $playerRatingValues;
}
private static function createPlayerTeamAssignmentMatrix($teamAssignmentsList, $totalPlayers)
{
// 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 = [];
$totalPreviousPlayers = 0;
2022-07-05 16:21:06 +02:00
$teamAssignmentsListCount = is_countable($teamAssignmentsList) ? count($teamAssignmentsList) : 0;
2010-10-03 17:42:31 -04:00
$currentColumn = 0;
for ($i = 0; $i < $teamAssignmentsListCount - 1; $i++) {
$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) : [];
foreach ($currentTeam->getAllPlayers() as $currentPlayer) {
2010-10-03 17:42:31 -04:00
$playerAssignments[$currentColumn][] = PartialPlay::getPartialPlayPercentage($currentPlayer);
// indicates the player is on the team
$totalPreviousPlayers++;
}
2010-10-03 17:42:31 -04:00
$rowsRemaining = $totalPlayers - $totalPreviousPlayers;
$nextTeam = $teamAssignmentsList[$i + 1];
foreach ($nextTeam->getAllPlayers() as $nextTeamPlayer) {
// Add a -1 * playing time to represent the difference
2010-10-03 17:42:31 -04:00
$playerAssignments[$currentColumn][] = -1 * PartialPlay::getPartialPlayPercentage($nextTeamPlayer);
$rowsRemaining--;
}
for ($ixAdditionalRow = 0; $ixAdditionalRow < $rowsRemaining; $ixAdditionalRow++) {
2010-10-03 17:42:31 -04:00
// Pad with zeros
$playerAssignments[$currentColumn][] = 0;
}
2010-10-03 17:42:31 -04:00
$currentColumn++;
}
2022-07-05 16:21:06 +02:00
return Matrix::fromColumnValues($totalPlayers, $teamAssignmentsListCount - 1, $playerAssignments);
}
2022-07-05 15:55:47 +02:00
}