Add tests for rating and fix bugs

This commit is contained in:
Dennis Eichhorn 2023-09-20 23:05:30 +00:00
parent 9843328a14
commit f5887798c2
6 changed files with 425 additions and 17 deletions

View File

@ -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),
];
}
}

View File

@ -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
{

View File

@ -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),

View File

@ -0,0 +1,96 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package tests
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace phpOMS\tests\Algorithm\Rating;
use phpOMS\Algorithm\Rating\Elo;
require_once __DIR__ . '/../../Autoloader.php';
/**
* @testdox phpOMS\tests\Algorithm\Rating\EloTest: Rating generation
*
* @internal
*/
final class EloTest extends \PHPUnit\Framework\TestCase
{
/**
* @testdox 1v1 rating test
* @covers phpOMS\Algorithm\Rating\Elo
* @group framework
*/
public function testSoloRating() : void
{
$rating = new Elo();
// large difference win
self::assertEquals(
['elo' => 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])
);
}
}

View File

@ -0,0 +1,140 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package tests
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace phpOMS\tests\Algorithm\Rating;
use phpOMS\Algorithm\Rating\Glicko1;
require_once __DIR__ . '/../../Autoloader.php';
/**
* @testdox phpOMS\tests\Algorithm\Rating\Glicko1Test: Rating generation
*
* @internal
*/
final class Glicko1Test extends \PHPUnit\Framework\TestCase
{
/**
* @testdox 1v1 rating test
* @covers phpOMS\Algorithm\Rating\Glicko1
* @group framework
*/
public function testSoloRating() : void
{
$rating = new Glicko1();
// large difference (no equal exchange due to RD)
self::assertEquals(
[
'elo' => 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])
);
}
}

View File

@ -0,0 +1,162 @@
<?php
/**
* Jingga
*
* PHP Version 8.1
*
* @package tests
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace phpOMS\tests\Algorithm\Rating;
use phpOMS\Algorithm\Rating\Glicko2;
require_once __DIR__ . '/../../Autoloader.php';
/**
* @testdox phpOMS\tests\Algorithm\Rating\Glicko2Test: Rating generation
*
* @internal
*/
final class Glicko2Test extends \PHPUnit\Framework\TestCase
{
/**
* @testdox 1v1 rating test
* @covers phpOMS\Algorithm\Rating\Glicko2
* @group framework
*/
public function testSoloRating() : void
{
$rating = new Glicko2();
// large difference (no equal exchange due to RD)
self::assertEqualsWithDelta(
[
'elo' => 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
);
}
}