diff --git a/README.md b/README.md new file mode 100644 index 0000000..0fdcf66 --- /dev/null +++ b/README.md @@ -0,0 +1,121 @@ +# Glicko2 rating system + +[![License](https://img.shields.io/github/license/laxity7/glicko2.svg)](https://github.com/laxity7/glicko2/blob/master/LICENSE) +[![Latest Stable Version](https://img.shields.io/packagist/v/laxity7/glicko2.svg)](https://packagist.org/packages/laxity7/glicko2) +[![Total Downloads](https://img.shields.io/packagist/dt/laxity7/glicko2.svg)](https://packagist.org/packages/laxity7/glicko2) + +A PHP implementation of [Glicko2 rating system](http://www.glicko.net/glicko.html) + +The Glicko2 rating system is a popular algorithm used to calculate the skill levels of players in various competitive +games. It takes into account both the player's performance and the strength of their opponents, providing more accurate +and up-to-date ratings compared to traditional methods like Elo. The system uses a logarithmic function that adjusts a +player's rating based on the outcome of their matches, with higher-skilled players having a greater impact on their +opponent's ratings. Glicko2 also employs a confidence interval and a smoothing parameter to further refine the accuracy +and stability of the rankings. + +The Glicko2 rating system is an advanced Elo system that was used in Mark Zuckerberg's infamous Facemash website + +Supports PHP versions 7.4 and 8.* + +## Installation + +For installation via Composer run: + +`composer require laxity7/glicko2` + +## Usage + +For example, you have a table in the database with the following fields: + +```php +use laxity7\glicko2\Player; +use App\UserRating; + +class UserRating +{ + public int $user_id; + public float $rating; + public float $rating_deviation; + public float $rating_volatility; +} + +class Repository +{ + public function getUserRating(int $userId) + { + $userRating = $this->findInDbFromId($userId); + if (!$userRating) { + $userRating = $this->createUserRating($userId); + } + return $userRating; + } + + public function createUserRating(int $userId) + { + $player = new Player(); + $userRating = new UserRating(); + $userRating->user_id = $userId; + $userRating->rating = $player->getRating(); + $userRating->rating_deviation = $player->getRatingDeviation(); + $userRating->rating_volatility = $player->getRatingVolatility(); + + $this->saveToDb($userRating); + + return $userRating; + } + + public function updateRating(int $userId, Player $player) + { + $userRating = $this->getUserRating($userId); + $userRating->rating = $player->getRating(); + $userRating->rating_deviation = $player->getRatingDeviation(); + $userRating->rating_volatility = $player->getRatingVolatility(); + + $this->saveToDb($userRating); + } +} +``` + +Ok, preparation completed, lets play + +```php +use laxity7\glicko2\MatchGame; +use laxity7\glicko2\MatchCollection; +use laxity7\glicko2\Player; + +$repository = new Repository(); + +$userRating1 = $repository->getUserRating(1); +$userRating2 = $repository->getUserRating(2); + +$player1 = new Player($userRating1->rating, $userRating1->rating_deviation, $userRating1->rating_volatility); +$player2 = new Player($userRating2->rating, $userRating2->rating_deviation, $userRating2->rating_volatility); +//$player2 = new Player(); available defaults for new player + +// match chain +$match1 = new MatchGame($player1, $player2, 1, 0); +$match1->calculate(); // The calculation method does not return anything, it only calculates and changes the rating of players + +$match2 = new MatchGame($player1, $player2, 3, 2); +$match2->calculate(); + +// or match collection +$matchCollection = new MatchCollection(); +$matchCollection->addMatch(new MatchGame($player1, $player2, 1, 0)); +$matchCollection->addMatch(new MatchGame($player1, $player2, 3, 2)); +$matchCollection->calculate(); + +// just get a new ratings +$newPlayer1Rating = $player1->getRating(); +$newPlayer2Rating = $player2->getRating(); + +// for example, save to the database +$repository->updateRating(1, $player1); +$repository->updateRating(2, $player2); +``` + +## Author + +[Aleksandr Zelenin](https://github.com/zelenin/), e-mail: [aleksandr@zelenin.me](mailto:aleksandr@zelenin.me) + +[Vlad Varlamov](https://github.com/laxity7/), e-mail: [vlad@varlamov.dev](mailto:vlad@varlamov.dev) diff --git a/composer.json b/composer.json index 9aa6bdb..9b72e03 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ }, { "name": "Vlad Varlamov", - "email": "work@laxity.ru", + "email": "vlad@varlamov.dev", "role": "Developer" } ], @@ -27,7 +27,7 @@ "source": "https://github.com/laxity7/glicko2" }, "require": { - "php": ">=7.1.0" + "php": "^7.4|^8.0" }, "require-dev": { "phpunit/phpunit": "~5.3.0" diff --git a/readme.md b/readme.md deleted file mode 100644 index 56bc39e..0000000 --- a/readme.md +++ /dev/null @@ -1,93 +0,0 @@ -# Glicko2 rating system - -[![License](https://img.shields.io/github/license/laxity7/glicko2.svg)](https://github.com/laxity7/glicko2/blob/master/LICENSE) -[![Latest Stable Version](https://img.shields.io/packagist/v/laxity7/glicko2.svg)](https://packagist.org/packages/laxity7/glicko2) -[![Total Downloads](https://img.shields.io/packagist/dt/laxity7/glicko2.svg)](https://packagist.org/packages/laxity7/glicko2) - -A PHP implementation of [Glicko2 rating system](http://www.glicko.net/glicko.html) - -## Installation - -### Composer - -The preferred way to install this extension is through [Composer](http://getcomposer.org/). - -Either run - -``` -php composer.phar require laxity7/glicko2 "~1.0.0" -``` - -or add - -``` -"laxity7/glicko2": "~1.0.0" -``` - -to the require section of your ```composer.json``` - -## Usage - -For ease of understanding take ActiveRecord pattern. - -Somewhere create object. Attribute names can be any - -```php -use laxity7\glicko2\Player; -// ... -public function createUserRating(int $userId) -{ - $userRating = new UserRating(); - $userRating->user_id = $userId; - $player = new Player(); - $userRating->rating = $player->getRating(); - $userRating->rating_deviation = $player->getRatingDeviation(); - $userRating->rating_volatility = $player->getRatingVolatility(); - - $userRating->insert(); - - return $userRating; -} -``` - -Ok, let's play - -```php -use laxity7\glicko2\Match; -use laxity7\glicko2\MatchCollection; -use laxity7\glicko2\Player; - -$player1 = new Player($userRating1->rating, $userRating1->rating_deviation, $userRating1->rating_volatility); -$player2 = new Player($userRating2->rating, $userRating2->rating_deviation, $userRating2->rating_volatility); - -// match chain -$match1 = new Match($player1, $player2, 1, 0); -$match1->calculate(); - -$match2 = new Match($player1, $player2, 3, 2); -$match2->calculate(); - -// or match collection -$matchCollection = new MatchCollection(); -$matchCollection->addMatch(new Match($player1, $player2, 1, 0)); -$matchCollection->addMatch(new Match($player1, $player2, 3, 2)); -$matchCollection->calculate(); - -$newPlayer1Rating = $player1->getRating(); -$newPlayer2Rating = $player2->getRating(); - -// for example, save in DB - -$userRating1->rating = $player1->getRating(); -$userRating1->rating_deviation = $player1->getRatingDeviation(); -$userRating1->rating_volatility = $player1->getRatingVolatility(); -$userRating1->update(); - -// similarly save the second player -``` - -## Author - -[Aleksandr Zelenin](https://github.com/zelenin/), e-mail: [aleksandr@zelenin.me](mailto:aleksandr@zelenin.me) - -[Vlad Varlamov](https://github.com/laxity7/), e-mail: [work@laxity.ru](mailto:work@laxity.ru) diff --git a/src/BaseMatch.php b/src/BaseMatch.php index c736efa..04a6b3d 100644 --- a/src/BaseMatch.php +++ b/src/BaseMatch.php @@ -4,17 +4,14 @@ abstract class BaseMatch { - /** @var Glicko2 */ - private $ratingSystem; + private Glicko2 $ratingSystem; /** * @return Glicko2 */ protected function getRatingSystem(): Glicko2 { - if ($this->ratingSystem === null) { - $this->ratingSystem = new Glicko2(); - } + $this->ratingSystem ??= new Glicko2(); return $this->ratingSystem; } @@ -22,5 +19,5 @@ protected function getRatingSystem(): Glicko2 /** * Calculate match */ - abstract public function calculate(); -} \ No newline at end of file + abstract public function calculate(): void; +} diff --git a/src/CalculationResult.php b/src/CalculationResult.php index 5d7c21a..d3ebe69 100644 --- a/src/CalculationResult.php +++ b/src/CalculationResult.php @@ -4,20 +4,14 @@ final class CalculationResult { - /** - * @var float - */ - private $mu; + /** @var float A rating μ */ + private float $mu; - /** - * @var float - */ - private $phi; + /** @var float A rating deviation φ */ + private float $phi; - /** - * @var float - */ - private $sigma; + /** @var float A rating volatility σ */ + private float $sigma; /** * @param float $mu @@ -32,7 +26,7 @@ public function __construct(float $mu, float $phi, float $sigma) } /** - * @return float + * @return float A rating μ */ public function getMu(): float { @@ -40,7 +34,7 @@ public function getMu(): float } /** - * @return float + * @return float A rating deviation φ */ public function getPhi(): float { @@ -48,7 +42,7 @@ public function getPhi(): float } /** - * @return float + * @return float A rating volatility σ */ public function getSigma(): float { diff --git a/src/Glicko2.php b/src/Glicko2.php index 3e4dfb9..d27465a 100644 --- a/src/Glicko2.php +++ b/src/Glicko2.php @@ -29,10 +29,8 @@ final class Glicko2 * results. If the application of Glicko-2 is expected to involve extremely improbable * collections of game outcomes, then τ should be set to a small value, even as small as, * say, τ = 0.2. - * - * @var float */ - private $tau; + private float $tau; /** * @param float $tau @@ -89,7 +87,7 @@ private function v(float $phiJ, float $mu, float $muJ): float */ private function g(float $phiJ): float { - return 1 / sqrt(1 + 3 * pow($phiJ, 2) / pow(M_PI, 2)); + return 1 / sqrt(1 + 3 * ($phiJ ** 2) / (M_PI ** 2)); } /** @@ -129,17 +127,17 @@ private function delta(float $phiJ, float $mu, float $muJ, float $score): float */ private function sigmaP(float $delta, float $sigma, float $phi, float $phiJ, float $mu, float $muJ): float { - $A = $a = log(pow($sigma, 2)); + $A = $a = log($sigma ** 2); $fX = function ($x, $delta, $phi, $v, $a, $tau) { - return ((exp($x) * (pow($delta, 2) - pow($phi, 2) - $v - exp($x))) / - (2 * pow((pow($phi, 2) + $v + exp($x)), 2))) - (($x - $a) / pow($tau, 2)); + return ((exp($x) * (($delta ** 2) - ($phi ** 2) - $v - exp($x))) / + (2 * ((($phi ** 2) + $v + exp($x)) ** 2))) - (($x - $a) / ($tau ** 2)); }; $epsilon = 0.000001; $v = $this->v($phiJ, $mu, $muJ); $tau = $this->tau; - if (pow($delta, 2) > (pow($phi, 2) + $v)) { - $B = log(pow($delta, 2) - pow($phi, 2) - $v); + if (($delta ** 2) > (($phi ** 2) + $v)) { + $B = log(($delta ** 2) - ($phi ** 2) - $v); } else { $k = 1; while ($fX($a - $k * $tau, $delta, $phi, $v, $a, $tau) < 0) { @@ -175,7 +173,7 @@ private function sigmaP(float $delta, float $sigma, float $phi, float $phiJ, flo */ private function phiS(float $phi, float $sigmaP): float { - return sqrt(pow($phi, 2) + pow($sigmaP, 2)); + return sqrt(($phi ** 2) + ($sigmaP ** 2)); } /** @@ -186,7 +184,7 @@ private function phiS(float $phi, float $sigmaP): float */ private function phiP(float $phiS, float $v): float { - return 1 / sqrt(1 / pow($phiS, 2) + 1 / $v); + return 1 / sqrt(1 / ($phiS ** 2) + 1 / $v); } /** @@ -200,6 +198,6 @@ private function phiP(float $phiS, float $v): float */ private function muP(float $mu, float $muJ, float $phiP, float $phiJ, float $score): float { - return $mu + pow($phiP, 2) * $this->g($phiJ) * ($score - $this->E($mu, $muJ, $phiJ)); + return $mu + ($phiP ** 2) * $this->g($phiJ) * ($score - $this->E($mu, $muJ, $phiJ)); } } diff --git a/src/MatchCollection.php b/src/MatchCollection.php index d8234a5..9accb01 100644 --- a/src/MatchCollection.php +++ b/src/MatchCollection.php @@ -8,9 +8,9 @@ class MatchCollection extends BaseMatch { /** - * @var ArrayObject|Match[] + * @var MatchGame[] */ - private $matches; + private ArrayObject $matches; public function __construct() { @@ -18,15 +18,15 @@ public function __construct() } /** - * @param Match $match + * @param MatchGame $match */ - public function addMatch(Match $match): void + public function addMatch(MatchGame $match): void { $this->matches->append($match); } /** - * @return ArrayIterator|Match[] + * @return ArrayIterator|MatchGame[] */ public function getMatches(): ArrayIterator { diff --git a/src/Match.php b/src/MatchGame.php similarity index 61% rename from src/Match.php rename to src/MatchGame.php index 79783f2..92588fd 100644 --- a/src/Match.php +++ b/src/MatchGame.php @@ -2,20 +2,21 @@ namespace laxity7\glicko2; -class Match extends BaseMatch +class MatchGame extends BaseMatch { - private const RESULT_WIN = 1; + /** The actual score for win */ + private const RESULT_WIN = 1.0; + /** The actual score for draw */ private const RESULT_DRAW = 0.5; - private const RESULT_LOSS = 0; + /** The actual score for loss */ + private const RESULT_LOSS = 0.0; - /** @var Player */ - private $player1; - /** @var Player */ - private $player2; - /** @var float */ - private $score1; - /** @var float */ - private $score2; + private Player $player1; + private Player $player2; + /** The player1 score */ + private float $score1; + /** The player2 score */ + private float $score2; /** * @param Player $player1 @@ -32,7 +33,9 @@ public function __construct(Player $player1, Player $player2, float $score1, flo } /** - * @return float + * Get the 1st player score + * + * @return float The 1st player score (0 for a loss, 0.5 for a draw, and 1 for a win) */ public function getScore(): float { @@ -49,10 +52,29 @@ public function getScore(): float break; } - return (float) $matchScore; + return $matchScore; } /** + * The winner of the match. Null if it's a draw. + * + * @return Player|null + */ + public function getWinner(): ?Player + { + switch ($this->getScore()) { + case self::RESULT_WIN: + return $this->player1; + case self::RESULT_LOSS: + return $this->player2; + default: + return null; + } + } + + /** + * Get the 1st player + * * @return Player */ public function getPlayer1(): Player @@ -61,6 +83,8 @@ public function getPlayer1(): Player } /** + * Get the 2nd player + * * @return Player */ public function getPlayer2(): Player @@ -69,7 +93,7 @@ public function getPlayer2(): Player } /** - * @param Match $match + * Calculate and change the rating of players */ public function calculate(): void { diff --git a/src/Player.php b/src/Player.php index 511ab8e..4458ad3 100644 --- a/src/Player.php +++ b/src/Player.php @@ -4,46 +4,37 @@ class Player { - protected const CONVERT = 173.7178; - public const DEFAULT_RATING = 1500; public const DEFAULT_RATING_DEVIATION = 350; public const DEFAULT_RATING_VOLATILITY = 0.06; + /** @var float Scaling factor */ + private const RATIO = 173.7178; + /** * A rating r - * - * @var float */ - private $rating; + private float $rating; /** * A rating μ - * - * @var float */ - private $ratingMu; + private float $ratingMu; /** * A rating deviation RD - * - * @var float */ - private $ratingDeviation; + private float $ratingDeviation; /** * A rating deviation φ - * - * @var float */ - private $ratingDeviationPhi; + private float $ratingDeviationPhi; /** * A rating volatility σ - * - * @var float */ - private $ratingVolatility; + private float $ratingVolatility; /** * Player constructor. @@ -68,7 +59,7 @@ public function __construct( private function setRating(float $rating): void { $this->rating = $rating; - $this->ratingMu = ($this->rating - static::DEFAULT_RATING) / self::CONVERT; + $this->ratingMu = ($this->rating - static::DEFAULT_RATING) / self::RATIO; } /** @@ -77,7 +68,7 @@ private function setRating(float $rating): void private function setRatingMu(float $mu): void { $this->ratingMu = $mu; - $this->rating = $this->ratingMu * self::CONVERT + static::DEFAULT_RATING; + $this->rating = $this->ratingMu * self::RATIO + static::DEFAULT_RATING; } /** @@ -86,7 +77,7 @@ private function setRatingMu(float $mu): void private function setRatingDeviation(float $ratingDeviation): void { $this->ratingDeviation = $ratingDeviation; - $this->ratingDeviationPhi = $this->ratingDeviation / self::CONVERT; + $this->ratingDeviationPhi = $this->ratingDeviation / self::RATIO; } /** @@ -95,7 +86,7 @@ private function setRatingDeviation(float $ratingDeviation): void private function setRatingDeviationPhi(float $phi): void { $this->ratingDeviationPhi = $phi; - $this->ratingDeviation = $this->ratingDeviationPhi * self::CONVERT; + $this->ratingDeviation = $this->ratingDeviationPhi * self::RATIO; } /** diff --git a/tests/Glicko2Test.php b/tests/Glicko2Test.php index 25b6d78..855278a 100644 --- a/tests/Glicko2Test.php +++ b/tests/Glicko2Test.php @@ -3,7 +3,7 @@ namespace laxity7\glicko2\Test; use PHPUnit_Framework_TestCase; -use laxity7\glicko2\Match; +use laxity7\glicko2\MatchGame; use laxity7\glicko2\MatchCollection; use laxity7\glicko2\Player; @@ -37,7 +37,7 @@ public function testCalculateMatch() $player1 = new Player(1500, 200, 0.06); $player2 = new Player(1400, 30, 0.06); - $match = new Match($player1, $player2, 1, 0); + $match = new MatchGame($player1, $player2, 1, 0); $match->calculate(); $this->assertEquals(1563.564, $this->round($player1->getRating())); @@ -57,14 +57,14 @@ public function testCalculateMatchCollection() $player3 = clone $player1; $player4 = clone $player2; - $match = new Match($player1, $player2, 1, 0); + $match = new MatchGame($player1, $player2, 1, 0); $match->calculate(); - $match = new Match($player1, $player2, 1, 0); + $match = new MatchGame($player1, $player2, 1, 0); $match->calculate(); $matchCollection = new MatchCollection(); - $matchCollection->addMatch(new Match($player3, $player4, 1, 0)); - $matchCollection->addMatch(new Match($player3, $player4, 1, 0)); + $matchCollection->addMatch(new MatchGame($player3, $player4, 1, 0)); + $matchCollection->addMatch(new MatchGame($player3, $player4, 1, 0)); $matchCollection->calculate(); $this->assertEquals($this->round($player1->getRating()), $this->round($player3->getRating()));