diff --git a/Algorithm/Rating/Elo.php b/Algorithm/Rating/Elo.php index 9c2a3bb3e..098fca94b 100644 --- a/Algorithm/Rating/Elo.php +++ b/Algorithm/Rating/Elo.php @@ -52,9 +52,9 @@ final class Elo /** * Calculate the elo rating * - * @param int $elo Current player elo - * @param int[] $oElo Current elo of all opponents - * @param int[] $s Match results against the opponents (1 = victor, 0 = loss, 0.5 = draw) + * @param int $elo Current player elo + * @param int[] $oElo Current elo of all opponents + * @param float[] $s Match results against the opponents (1 = victor, 0 = loss, 0.5 = draw) * * @return array{elo:int} * @@ -67,11 +67,11 @@ final class Elo $expected = 1 / (1 + 10 ** (($o - $elo) / 400)); $r = $this->K * ($s[$idx] - $expected); - $eloNew += $r; + $eloNew += (int) \round($r); } return [ - 'elo' => (int) \max((int) $eloNew, $this->MIN_ELO), + 'elo' => (int) \max($eloNew, $this->MIN_ELO), ]; } } diff --git a/Algorithm/Rating/Glicko1.php b/Algorithm/Rating/Glicko1.php index 1c01ce197..48331c47d 100644 --- a/Algorithm/Rating/Glicko1.php +++ b/Algorithm/Rating/Glicko1.php @@ -108,8 +108,8 @@ final class Glicko1 * @param int $rdOld Current player deviation (RD) * @param int $lastMatchDate Last match date used to calculate the time difference (can be days, months, ... depending on your match interval) * @param int $matchDate Match date (usually day) - * @param float[] $s Match results (1 = victor, 0 = loss, 0.5 = draw) * @param int[] $oElo Opponent "elo" + * @param float[] $s Match results (1 = victor, 0 = loss, 0.5 = draw) * @param int[] $oRd Opponent deviation (RD) * * @return array{elo:int, rd:int} @@ -121,8 +121,8 @@ final class Glicko1 int $rdOld = 50, int $lastMatchDate = 0, int $matchDate = 0, - array $s = [], array $oElo = [], + array $s = [], array $oRd = [] ) : array { diff --git a/Algorithm/Rating/Glicko2.php b/Algorithm/Rating/Glicko2.php index 1abea5668..089164925 100644 --- a/Algorithm/Rating/Glicko2.php +++ b/Algorithm/Rating/Glicko2.php @@ -30,6 +30,16 @@ use phpOMS\Math\Solver\Root\Bisection; */ final class Glicko2 { + /** + * Glicko scale factor + * + * @latex Q = 400 / ln(10) + * + * @var int + * @since 1.0.0 + */ + private const Q = 173.7177927613; + /** * Constraint for the volatility over time (smaller = stronger constraint) * @@ -90,8 +100,8 @@ final class Glicko2 * @param int $elo Current player "elo" * @param int $rdOld Current player deviation (RD) * @param float $volOld Last match date used to calculate the time difference (can be days, months, ... depending on your match interval) - * @param float[] $s Match results (1 = victor, 0 = loss, 0.5 = draw) * @param int[] $oElo Opponent "elo" + * @param float[] $s Match results (1 = victor, 0 = loss, 0.5 = draw) * @param int[] $oRd Opponent deviation (RD) * * @return array{elo:int, rd:int, vol:float} @@ -102,23 +112,23 @@ final class Glicko2 int $elo = 1500, int $rdOld = 50, float $volOld = 0.06, - array $s = [], array $oElo = [], + array $s = [], array $oRd = [] ) : array { $tau = $this->tau; // Step 0: - $rdOld = $rdOld / 173.7178; - $elo = ($elo - $this->DEFAULT_ELO) / 173.7178; + $rdOld = $rdOld / self::Q; + $elo = ($elo - $this->DEFAULT_ELO) / self::Q; foreach ($oElo as $idx => $value) { - $oElo[$idx] = ($value - $this->DEFAULT_ELO) / 173.7178; + $oElo[$idx] = ($value - $this->DEFAULT_ELO) / self::Q; } foreach ($oRd as $idx => $value) { - $oRd[$idx] = $value / 173.7178; + $oRd[$idx] = $value / self::Q; } // Step 1: @@ -128,8 +138,8 @@ final class Glicko2 } $E = []; - foreach ($oElo as $idx => $elo) { - $E[$idx] = 1 / (1 + \exp(-$g[$idx] * ($elo - $elo))); + foreach ($oElo as $idx => $oe) { + $E[$idx] = 1 / (1 + \exp(-$g[$idx] * ($elo - $oe))); } $v = 0; @@ -159,8 +169,8 @@ final class Glicko2 $r = $elo + $RD ** 2 * $tDelta; // Undo step 0: - $RD = 173.7178 * $RD; - $r = 173.7178 * $r + $this->DEFAULT_ELO; + $RD = self::Q * $RD; + $r = self::Q * $r + $this->DEFAULT_ELO; return [ 'elo' => (int) \max($r, $this->MIN_ELO), diff --git a/tests/Algorithm/Rating/EloTest.php b/tests/Algorithm/Rating/EloTest.php new file mode 100644 index 000000000..49a2994be --- /dev/null +++ b/tests/Algorithm/Rating/EloTest.php @@ -0,0 +1,96 @@ + 1805], + $rating->rating(1800, [1500], [1.0]) + ); + + self::assertEquals( + ['elo' => 1495], + $rating->rating(1500, [1800], [0.0]) + ); + + // similar win + self::assertEquals( + ['elo' => 1564], + $rating->rating(1550, [1500], [1.0]) + ); + + self::assertEquals( + ['elo' => 1486], + $rating->rating(1500, [1550], [0.0]) + ); + + // similar draw + self::assertEquals( + ['elo' => 1548], + $rating->rating(1550, [1500], [0.5]) + ); + + self::assertEquals( + ['elo' => 1502], + $rating->rating(1500, [1550], [0.5]) + ); + + // very large difference win + self::assertEquals( + ['elo' => 2400], + $rating->rating(2400, [1500], [1.0]) + ); + + self::assertEquals( + ['elo' => 1500], + $rating->rating(1500, [2400], [0.0]) + ); + } + + /** + * @testdox group rating test + * @covers phpOMS\Algorithm\Rating\Elo + * @group framework + */ + public function testGroupRating() : void + { + $rating = new Elo(); + + self::assertEquals( + ['elo' => 1500 + 18 + 4 + 14 - 16], + $rating->rating(1500, [1550, 1600, 1450, 1500], [1.0, 0.5, 1.0, 0.0]) + ); + } +} diff --git a/tests/Algorithm/Rating/Glicko1Test.php b/tests/Algorithm/Rating/Glicko1Test.php new file mode 100644 index 000000000..1d2d9bfd7 --- /dev/null +++ b/tests/Algorithm/Rating/Glicko1Test.php @@ -0,0 +1,140 @@ + 1648, + 'rd' => 186, + ], + $rating->rating(1500, 200, 0, 0, [1800], [1.0], [150]) + ); + + self::assertEquals( + [ + 'elo' => 1717, + 'rd' => 144, + ], + $rating->rating(1800, 150, 0, 0, [1500], [0.0], [200]) + ); + + // large difference (equal exchange due to same RD) + self::assertEquals( + [ + 'elo' => 1637, + 'rd' => 186, + ], + $rating->rating(1500, 200, 0, 0, [1800], [1.0], [200]) + ); + + self::assertEquals( + [ + 'elo' => 1662, + 'rd' => 186, + ], + $rating->rating(1800, 200, 0, 0, [1500], [0.0], [200]) + ); + + // similar win + self::assertEquals( + [ + 'elo' => 1621, + 'rd' => 177, + ], + $rating->rating(1550, 200, 0, 0, [1500], [1.0], [150]) + ); + + self::assertEquals( + [ + 'elo' => 1428, + 'rd' => 177, + ], + $rating->rating(1500, 200, 0, 0, [1550], [0.0], [150]) + ); + + // similar draw + self::assertEquals( + [ + 'elo' => 1539, + 'rd' => 177, + ], + $rating->rating(1550, 200, 0, 0, [1500], [0.5], [150]) + ); + + self::assertEquals( + [ + 'elo' => 1510, + 'rd' => 177, + ], + $rating->rating(1500, 200, 0, 0, [1550], [0.5], [150]) + ); + + // very large difference win + self::assertEquals( + [ + 'elo' => 2401, + 'rd' => 199, + ], + $rating->rating(2400, 200, 0, 0, [1500], [1.0], [150]) + ); + + self::assertEquals( + [ + 'elo' => 1498, + 'rd' => 199, + ], + $rating->rating(1500, 200, 0, 0, [2400], [0.0], [150]) + ); + } + + /** + * @testdox group rating test + * @covers phpOMS\Algorithm\Rating\Glicko1 + * @group framework + */ + public function testGroupRating() : void + { + $rating = new Glicko1(); + + self::assertEquals( + [ + 'elo' => 1464, + 'rd' => 151, + ], + $rating->rating(1500, 200, 0, 0, [1400, 1550, 1700], [1.0, 0.0, 0.0], [30, 100, 300]) + ); + } +} diff --git a/tests/Algorithm/Rating/Glicko2Test.php b/tests/Algorithm/Rating/Glicko2Test.php new file mode 100644 index 000000000..25c2ff12b --- /dev/null +++ b/tests/Algorithm/Rating/Glicko2Test.php @@ -0,0 +1,162 @@ + 1649, + 'rd' => 186, + 'vol' => 0.06 + ], + $rating->rating(1500, 200, 0.06, [1800], [1.0], [150]), + 0.0001 + ); + + self::assertEqualsWithDelta( + [ + 'elo' => 1717, + 'rd' => 144, + 'vol' => 0.06 + ], + $rating->rating(1800, 150, 0.06, [1500], [0.0], [200]), + 0.0001 + ); + + // large difference (equal exchange due to same RD) + self::assertEqualsWithDelta( + [ + 'elo' => 1638, + 'rd' => 187, + 'vol' => 0.06 + ], + $rating->rating(1500, 200, 0.06, [1800], [1.0], [200]), + 0.0001 + ); + + self::assertEqualsWithDelta( + [ + 'elo' => 1661, + 'rd' => 187, + 'vol' => 0.06 + ], + $rating->rating(1800, 200, 0.06, [1500], [0.0], [200]), + 0.0001 + ); + + // similar win + self::assertEqualsWithDelta( + [ + 'elo' => 1621, + 'rd' => 177, + 'vol' => 0.06 + ], + $rating->rating(1550, 200, 0.06, [1500], [1.0], [150]), + 0.0001 + ); + + self::assertEqualsWithDelta( + [ + 'elo' => 1428, + 'rd' => 177, + 'vol' => 0.06 + ], + $rating->rating(1500, 200, 0.06, [1550], [0.0], [150]), + 0.0001 + ); + + // similar draw + self::assertEqualsWithDelta( + [ + 'elo' => 1539, + 'rd' => 177, + 'vol' => 0.06 + ], + $rating->rating(1550, 200, 0.06, [1500], [0.5], [150]), + 0.0001 + ); + + self::assertEqualsWithDelta( + [ + 'elo' => 1510, + 'rd' => 177, + 'vol' => 0.06 + ], + $rating->rating(1500, 200, 0.06, [1550], [0.5], [150]), + 0.0001 + ); + + // very large difference win + self::assertEqualsWithDelta( + [ + 'elo' => 2401, + 'rd' => 199, + 'vol' => 0.06 + ], + $rating->rating(2400, 200, 0.06, [1500], [1.0], [150]), + 0.0001 + ); + + self::assertEqualsWithDelta( + [ + 'elo' => 1498, + 'rd' => 199, + 'vol' => 0.06 + ], + $rating->rating(1500, 200, 0.06, [2400], [0.0], [150]), + 0.0001 + ); + } + + /** + * @testdox group rating test + * @covers phpOMS\Algorithm\Rating\Glicko2 + * @group framework + */ + public function testGroupRating() : void + { + $rating = new Glicko2(); + + self::assertEqualsWithDelta( + [ + 'elo' => 1464, + 'rd' => 151, + 'vol' => 0.059999 + ], + $rating->rating(1500, 200, 0.06, [1400, 1550, 1700], [1.0, 0.0, 0.0], [30, 100, 300]), + 0.0001 + ); + } +}