Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.67% covered (success)
96.67%
29 / 30
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
Glicko1
96.67% covered (success)
96.67%
29 / 30
50.00% covered (danger)
50.00%
1 / 2
5
0.00% covered (danger)
0.00%
0 / 1
 calculateC
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 rating
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Algorithm\Rating
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\Algorithm\Rating;
16
17/**
18 * Elo rating calculation using Glicko-1
19 *
20 * @package phpOMS\Algorithm\Rating
21 * @license OMS License 2.0
22 * @link    https://jingga.app
23 * @see     https://en.wikipedia.org/wiki/Glicko_rating_system
24 * @see     http://www.glicko.net/glicko/glicko.pdf
25 * @since   1.0.0
26 */
27final class Glicko1
28{
29    /**
30     * Helper constant
31     *
32     * @latex Q = ln(10) / 400
33     *
34     * @var int
35     * @since 1.0.0
36     */
37    private const Q = 0.00575646273;
38
39    /**
40     * Default elo to use for new players
41     *
42     * @var int
43     * @since 1.0.0
44     */
45    public int $DEFAULT_ELO = 1500;
46
47    /**
48     * Default rd to use for new players
49     *
50     * @var int
51     * @since 1.0.0
52     */
53    public int $DEFAULT_RD = 350;
54
55    /**
56     * C (constant) for RD caclulation
57     *
58     * This is used to adjust the RD value based on the time from the last time a player played a match
59     *
60     * @latex RD = min\left(\sqrt{RD_0^2 + c^2t}, 350\right)
61     *
62     * @see calculateC();
63     *
64     * @var float
65     * @since 1.0.0
66     */
67    public float $DEFAULT_C = 34.6;
68
69    /**
70     * Lowest elo allowed
71     *
72     * @var int
73     * @since 1.0.0
74     */
75    public int $MIN_ELO = 100;
76
77    /**
78     * Lowest rd allowed
79     *
80     * @example 50 means that the player rating is probably between -100 / +100 of the current rating
81     *
82     * @var int
83     * @since 1.0.0
84     */
85    public int $MIN_RD = 50;
86
87    /**
88     * Calculate the C value.
89     *
90     * This is only necessary if you change the DEFAULT_RD, want a different rating period or have significantly different average RD values.
91     *
92     * @param int $ratingPeriods Time without matches until the RD returns to the default RD
93     * @param int $avgRD         Average RD
94     *
95     * @return void
96     *
97     * @since 1.0.0
98     */
99    public function calculateC(int $ratingPeriods = 100, int $avgRD = 50) : void
100    {
101        $this->DEFAULT_C = \sqrt(($this->DEFAULT_RD ** 2 - $avgRD ** 2) / $ratingPeriods);
102    }
103
104    /**
105     * Calcualte the glicko-1 elo
106     *
107     * @param int     $elo           Current player "elo"
108     * @param int     $rdOld         Current player deviation (RD)
109     * @param int     $lastMatchDate Last match date used to calculate the time difference (can be days, months, ... depending on your match interval)
110     * @param int     $matchDate     Match date (usually day)
111     * @param int[]   $oElo          Opponent "elo"
112     * @param float[] $s             Match results (1 = victor, 0 = loss, 0.5 = draw)
113     * @param int[]   $oRd           Opponent deviation (RD)
114     *
115     * @return array{elo:int, rd:int}
116     *
117     * @since 1.0.0
118     */
119    public function rating(
120        int $elo = 1500,
121        int $rdOld = 50,
122        int $lastMatchDate = 0,
123        int $matchDate = 0,
124        array $oElo = [],
125        array $s = [],
126        array $oRd = []
127    ) : array
128    {
129        // Step 1:
130        $E   = [];
131        $gRD = [];
132
133        $RD = \min(
134            350,
135            \max(
136                \sqrt(
137                    $rdOld * $rdOld
138                    + $this->DEFAULT_C * $this->DEFAULT_C * \max(0, $matchDate - $lastMatchDate)
139                ),
140                $this->MIN_RD
141            )
142        );
143
144        // Step 2:
145        foreach ($oElo as $id => $e) {
146            $gRD_t    = 1 / (\sqrt(1 + 3 * self::Q * self::Q * $oRd[$id] * $oRd[$id] / (\M_PI * \M_PI)));
147            $gRD[$id] = $gRD_t;
148            $E[$id]   = 1 / (1 + \pow(10, $gRD_t * ($elo - $e) / -400));
149        }
150
151        $d = 0;
152        foreach ($E as $id => $_) {
153            $d += $gRD[$id] * $gRD[$id] * $E[$id] * (1 - $E[$id]);
154        }
155        $d2 = 1 / (self::Q * self::Q * $d);
156
157        $r = 0;
158        foreach ($E as $id => $_) {
159            $r += $gRD[$id] * ($s[$id] - $E[$id]);
160        }
161        $r = $elo + self::Q / (1 / ($RD * $RD) + 1 / $d2) * $r;
162
163        // Step 3:
164        $RD_ = \sqrt(1 / (1 / ($RD * $RD) + 1 / $d2));
165
166        return [
167            'elo' => (int) \max((int) $r, $this->MIN_ELO),
168            'rd'  => (int) \max($RD_, $this->MIN_RD),
169        ];
170    }
171}