Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
95.00% covered (success)
95.00%
57 / 60
81.25% covered (warning)
81.25%
13 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Rating
94.92% covered (success)
94.92%
56 / 59
81.25% covered (warning)
81.25%
13 / 16
34.15
0.00% covered (danger)
0.00%
0 / 1
 getReadableRank
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 isValidReadableRank
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
6.02
 getRankFromReadableRank
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 getRankFromRating
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getReadableRankFromRating
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRankMinimalRating
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getRankMinimalRatingFromReadableRank
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRankMiddleRatingFromRank
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRankMiddleRatingFromReadableRank
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 beta
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 calculateRatingChange
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 ratingToXP
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 ratingToXPFloat
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 parseRatingOrReadableRank
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 isReasonableRating
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getReadableRankFromRatingWhenPossible
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3use function PHPUnit\Framework\isNull;
4
5App::uses('RatingParseException', 'Utility');
6
7class Rating
8{
9    public static function getReadableRank(int $rank): string
10    {
11        if ($rank <= 30)
12            return (string) (31 - $rank) . 'k';
13
14        return (string) ($rank - 30) . 'd';
15    }
16
17    public static function isValidReadableRank(string $readableRank): bool
18    {
19        if (strlen($readableRank) < 2)
20            return false;
21        $suffix = substr($readableRank, -1);
22        $numberStr = substr($readableRank, 0, -1);
23        if (!ctype_digit($numberStr))
24            return false;
25        $number = (int) $numberStr;
26        if ($suffix === 'k')
27            return $number >= 1 && $number <= 30;
28        if ($suffix === 'd')
29            return $number >= 1;
30        return false;
31    }
32
33    public static function getRankFromReadableRank(string $readableRank): int
34    {
35        $suffix = substr($readableRank, -1);
36        $number = substr($readableRank, 0, -1);
37        if (!ctype_digit($number))
38            throw new RatingParseException($readableRank . " can't be parsed as go rank.");
39        if ($suffix === 'k')
40            return 31 - (int) $number;
41        elseif ($suffix === 'd')
42            return 30 + (int) $number;
43        else
44            throw new RatingParseException($readableRank . " can't be parsed as go rank.");
45    }
46
47    public static function getRankFromRating(float $rating): int
48    {
49        // Internal number for rank representation better than the textual "18k" etc, so it is just going to be integer like this
50        // 30k   = rating [-950, -850) = rank  1
51        // "20k" = rating [  50,  150) = rank 11
52        // "10k" = rating [1050, 1150) = rank 21
53        //  1k   = rating [1950, 2050) = rank 30
54        //  1d   = rating [2050, 2150) = rank 31
55        //  7d   = rating [2650, 2750) = rank 37 7d is equivalent of pro rank, and then it is custom to have 30 points per rank
56        //  8d   = rating [2750, 2780) = rank 38
57        //  9d   = rating [2780, 2810) = rank 39
58        // 10d   = rating [2780, 2810) = rank 40
59        // 11d   = rating [2810, 2840) = rank 41
60        // .....
61        if ($rating < 2750)
62            return (int) floor(max(($rating + 1050) / 100, 1));
63
64        return (int) floor(($rating - 2750) / 30) + 38;
65    }
66
67    public static function getReadableRankFromRating(float $rating): string
68    {
69        return static::getReadableRank(static::getRankFromRating($rating));
70    }
71
72    public static function getRankMinimalRating(int $rank): float
73    {
74        if ($rank <= 38)
75            return 100 * $rank - 1050.0;
76        return ($rank - 38) * 30 + 2750.0;
77    }
78
79    public static function getRankMinimalRatingFromReadableRank(string $readableRank): float
80    {
81        return Rating::getRankMinimalRating(Rating::getRankFromReadableRank($readableRank));
82    }
83
84    public static function getRankMiddleRatingFromRank(int $rank): float
85    {
86        return (Rating::getRankMinimalRating($rank) + Rating::getRankMinimalRating($rank + 1)) / 2;
87    }
88
89    public static function getRankMiddleRatingFromReadableRank(string $readableRank): float
90    {
91        return Rating::getRankMiddleRatingFromRank(Rating::getRankFromReadableRank($readableRank));
92    }
93
94    private static function beta($rating)
95    {
96        return -7 * log(3300 - $rating);
97    }
98
99    public static function calculateRatingChange($rating, $opponentRating, $result, $modifier)
100    {
101        $Se = 1.0 / (1.0 + exp(self::beta($opponentRating) - self::beta($rating)));
102        $con = pow(((3300 - $rating) / 200), 1.6);
103        $bonus = log(1 + exp((2300 - $rating) / 80)) / 5;
104        return $modifier * ($con * ($result - $Se) + $bonus);
105    }
106
107    // changes should be reflected in util.js
108    public static function ratingToXP(float $rating, float $multiplier): int
109    {
110        $bla = intval(ceil(Rating::ratingToXPFloat($rating) * $multiplier));
111        return intval(ceil(Rating::ratingToXPFloat($rating) * $multiplier));
112    }
113
114    public static function ratingToXPFloat(float $rating): float
115    {
116        if ($rating < 0)
117            return 1 + max(0, ($rating / 1000 + 0.9) * 3);
118
119        // until 1200 rating, the old formula but with half of the values
120        if ($rating < 1200)
121            return max(10, pow($rating / 100, 1.55) - 6) / 2;
122
123        // with higher ratings, it is important to have more aggressive exponential growth,
124        return (pow(($rating - 500) / 100, 2) - 10) / 2;
125    }
126
127    public static function parseRatingOrReadableRank(string $input): float
128    {
129        if (is_numeric($input))
130        {
131            if (!self::isReasonableRating((float) $input))
132                throw new RatingParseException("Rating of " . $input . "isn't reasonable");
133            return (float) $input;
134        }
135        return self::getRankMiddleRatingFromReadableRank($input);
136    }
137
138    public static function isReasonableRating(float $rating)
139    {
140        return $rating >= -950 // 30k
141                && $rating < 3200; // the formula stops working at 3300
142    }
143
144    public static function getReadableRankFromRatingWhenPossible(?float $rating): string
145    {
146        if (is_null($rating))
147            return '';
148        $rank = self::getRankFromRating($rating);
149        if (self::getRankMiddleRatingFromRank($rank) == $rating)
150            return Rating::getReadableRank($rank);
151        return strval($rating);
152    }
153}