RatingCalculator::getEloWinProbability()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace App\Models\Rating;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Support\Facades\DB;
7
use Illuminate\Support\Facades\Hash;
8
use App\Models\ContestModel;
9
use Cache;
10
use Storage;
11
use Log;
12
13
class RatingCalculator extends Model
14
{
15
    public $cid=0;
16
    public $contestants=[];
17
    public $totParticipants=0;
18
    public $INITIAL_RATING=1500;
19
20
    public function __construct($cid) {
21
        $this->cid=$cid;
22
23
        // get rank
24
        $this->getRecord();
25
    }
26
27
    private function getRecord() {
28
        $contestRankRaw=Cache::tags(['contest', 'rank'])->get($this->cid);
29
30
        if ($contestRankRaw==null) {
31
            $contestModel=new ContestModel();
32
            $contestRankRaw=$contestModel->contestRankCache($this->cid);
33
        }
34
35
        $this->totParticipants=count($contestRankRaw);
36
        foreach ($contestRankRaw as $c) {
37
            $this->contestants[]=[
38
                "uid"=>$c["uid"],
39
                "points"=>$c["score"],
40
                "rating"=>DB::table("users")->where(["id"=>$c["uid"]])->first()["professional_rate"]
41
            ];
42
        }
43
    }
44
45
    private function reassignRank() {
46
        $this->sort("points");
47
        $idx=0;
48
        $points=$this->contestants[0]["points"];
49
        $i=1;
50
        while ($i<$this->totParticipants) {
51
            if ($this->contestants[$i]["points"]<$points) {
52
                $j=$idx;
53
                while ($j<$i) {
54
                    $this->contestants[$j]["rank"]=$i;
55
                    $j+=1;
56
                }
57
                $idx=$i;
58
                $points=$this->contestants[$i]["points"];
59
            }
60
            $i+=1;
61
        }
62
        $j=$idx;
63
        while ($j<$this->totParticipants) {
64
            $this->contestants[$j]["rank"]=$this->totParticipants;
65
            $j+=1;
66
        }
67
    }
68
69
    private function getEloWinProbability($Ra, $Rb) {
70
        return 1.0 / (1+pow(10, ($Rb-$Ra) / 400.0));
71
    }
72
73
    private function getSeed($rating) {
74
        $result=1.0;
75
        foreach ($this->contestants as $other) {
76
            $result+=$this->getEloWinProbability($other["rating"], $rating);
77
        }
78
        return $result;
79
    }
80
81
    private function getRatingToRank($rank) {
82
        $left=1;
83
        $right=8000;
84
        while ($right-$left>1) {
85
            $mid=floor(($right+$left) / 2);
86
            if ($this->getSeed($mid)<$rank) {
87
                $right=$mid;
88
            } else {
89
                $left=$mid;
90
            }
91
        }
92
        return $left;
93
    }
94
95
    private function sort($key) {
96
        usort($this->contestants, function($a, $b) use ($key) {
97
            return $b[$key] <=> $a[$key];
98
        });
99
    }
100
101
    public function calculate() {
102
        if (empty($this->contestants)) {
103
            return;
104
        }
105
106
        // recalc rank
107
        $this->reassignRank();
108
109
        foreach ($this->contestants as &$member) {
110
            $member["seed"]=1.0;
111
            foreach ($this->contestants as $other) {
112
                if ($member["uid"]!=$other["uid"]) {
113
                    $member["seed"]+=$this->getEloWinProbability($other["rating"], $member["rating"]);
114
                }
115
            }
116
        }
117
        unset($member);
118
119
        foreach ($this->contestants as &$contestant) {
120
            $midRank=sqrt($contestant["rank"] * $contestant["seed"]);
121
            $contestant["needRating"]=$this->getRatingToRank($midRank);
122
            $contestant["delta"]=floor(($contestant["needRating"]-$contestant["rating"]) / 2);
123
        }
124
        unset($contestant);
125
126
        $this->sort("rating");
127
128
        // DO some adjuct
129
        // Total sum should not be more than ZERO.
130
        $sum=0;
131
132
        foreach ($this->contestants as $contestant) {
133
            $sum+=$contestant["delta"];
134
        }
135
        $inc=-floor($sum / $this->totParticipants)-1;
136
        foreach ($this->contestants as &$contestant) {
137
            $contestant["delta"]+=$inc;
138
        }
139
        unset($contestant);
140
141
        // Sum of top-4*sqrt should be adjusted to ZERO.
142
143
        $sum=0;
144
        $zeroSumCount=min(intval(4 * round(sqrt($this->totParticipants))), $this->totParticipants);
145
146
        for ($i=0; $i<$zeroSumCount; $i++) {
147
            $sum+=$this->contestants[$i]["delta"];
148
        }
149
150
        $inc=min(max(-floor($sum / $zeroSumCount), -10), 0);
151
152
        for ($i=0; $i<$zeroSumCount; $i++) {
153
            $this->contestants[$i]["delta"]+=$inc;
154
        }
155
156
        return $this->validateDeltas();
157
    }
158
159
    public function storage() {
160
        $contestants=$this->contestants;
161
        DB::transaction(function() use ($contestants) {
162
            foreach ($contestants as $contestant) {
163
                $newRating=$contestant["rating"]+$contestant["delta"];
164
                DB::table("users")->where([
165
                    "id"=>$contestant["uid"]
166
                ])->update([
167
                    "professional_rate"=>$newRating
168
                ]);
169
                DB::table("professional_rated_change_log")->insert([
170
                    "uid"=>$contestant["uid"],
171
                    "cid"=>$this->cid,
172
                    "rated"=>$newRating
173
                ]);
174
            }
175
            // Mark
176
            DB::table("contest")->where([
177
                "cid"=>$this->cid
178
            ])->update([
179
                "is_rated"=>1
180
            ]);
181
        }, 5);
182
    }
183
184
    private function validateDeltas() {
185
        $this->sort("points");
186
187
        for ($i=0; $i<$this->totParticipants; $i++) {
188
            for ($j=$i+1; $j<$this->totParticipants; $j++) {
189
                if ($this->contestants[$i]["rating"]>$this->contestants[$j]["rating"]) {
190
                    if ($this->contestants[$i]["rating"]+$this->contestants[$i]["delta"]<$this->contestants[$j]["rating"]+$this->contestants[$j]["delta"]) {
191
                        Log::warning("First rating invariant failed: {$this->contestants[$i]["uid"]} vs. {$this->contestants[$j]["uid"]}.");
192
                        return false;
193
                    }
194
                }
195
196
                if ($this->contestants[$i]["rating"]<$this->contestants[$j]["rating"]) {
197
                    if ($this->contestants[$i]["delta"]<$this->contestants[$j]["delta"]) {
198
                        Log::warning("Second rating invariant failed: {$this->contestants[$i]["uid"]} vs.  {$this->contestants[$j]["uid"]}.");
199
                        return false;
200
                    }
201
                }
202
            }
203
        }
204
        return true;
205
    }
206
207
}
208