GroupRatingCalculator::getRecord()   B
last analyzed

Complexity

Conditions 7
Paths 18

Size

Total Lines 31
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 24
dl 0
loc 31
c 0
b 0
f 0
rs 8.6026
cc 7
nc 18
nop 0
1
<?php
2
3
namespace App\Models\Rating;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Illuminate\Support\Facades\DB;
7
use App\Models\ContestModel;
8
use Log;
9
10
class GroupRatingCalculator extends Model
11
{
12
    public $cid=0;
13
    public $contestants=[];
14
    public $totParticipants=0;
15
    public $INITIAL_RATING=1500;
16
17
    public function __construct($cid) {
18
        $contestModel=new ContestModel();
19
        $this->cid=$cid;
20
        $this->gid=$contestModel->gid($cid);
0 ignored issues
show
Bug introduced by
The property gid does not seem to exist on App\Models\Rating\GroupRatingCalculator. Are you sure there is no database migration missing?

Checks if undeclared accessed properties appear in database migrations and if the creating migration is correct.

Loading history...
21
        // get rank
22
        $this->getRecord();
23
    }
24
25
    private function getRecord() {
26
        $contestModel=new ContestModel();
27
        $contestRankRaw=$contestModel->contestRank($this->cid);
28
        foreach ($contestRankRaw as $key => $contestRank) {
29
            if (isset($contestRank['remote']) && $contestRank['remote']) {
30
                unset($contestRankRaw[$key]);
31
            }
32
        }
33
        $contestRankRaw=array_values($contestRankRaw);
34
        $members=array_column($contestRankRaw, 'uid');
35
        $ratings_temp=DB::table('group_member')
36
            ->where([
37
                'gid' => $this->gid,
38
            ])->whereIn('uid', $members)
39
            ->select('uid', 'ranking')
40
            ->get()->all();
41
        $ratings=[];
42
        foreach ($ratings_temp as $rating) {
43
            $ratings[$rating['uid']]=$rating['ranking'];
44
        }
45
        foreach ($contestRankRaw as $c) {
46
            if (!isset($ratings[$c['uid']])) {
47
                continue;
48
            }
49
            $this->contestants[]=[
50
                "uid"=>$c["uid"],
51
                "points"=>$c["score"],
52
                "rating"=>$ratings[$c['uid']],
53
            ];
54
        }
55
        $this->totParticipants=count($this->contestants);
56
    }
57
58
    private function reassignRank() {
59
        $this->sort("points");
60
        $idx=0;
61
        $points=$this->contestants[0]["points"];
62
        $i=1;
63
        while ($i<$this->totParticipants) {
64
            if ($this->contestants[$i]["points"]<$points) {
65
                $j=$idx;
66
                while ($j<$i) {
67
                    $this->contestants[$j]["rank"]=$i;
68
                    $j+=1;
69
                }
70
                $idx=$i;
71
                $points=$this->contestants[$i]["points"];
72
            }
73
            $i+=1;
74
        }
75
        $j=$idx;
76
        while ($j<$this->totParticipants) {
77
            $this->contestants[$j]["rank"]=$this->totParticipants;
78
            $j+=1;
79
        }
80
    }
81
82
    private function getEloWinProbability($Ra, $Rb) {
83
        return 1.0 / (1+pow(10, ($Rb-$Ra) / 400.0));
84
    }
85
86
    private function getSeed($rating) {
87
        $result=1.0;
88
        foreach ($this->contestants as $other) {
89
            $result+=$this->getEloWinProbability($other["rating"], $rating);
90
        }
91
        return $result;
92
    }
93
94
    private function getRatingToRank($rank) {
95
        $left=1;
96
        $right=8000;
97
        while ($right-$left>1) {
98
            $mid=floor(($right+$left) / 2);
99
            if ($this->getSeed($mid)<$rank) {
100
                $right=$mid;
101
            } else {
102
                $left=$mid;
103
            }
104
        }
105
        return $left;
106
    }
107
108
    private function sort($key) {
109
        usort($this->contestants, function($a, $b) use ($key) {
110
            return $b[$key] <=> $a[$key];
111
        });
112
    }
113
114
    public function calculate() {
115
        if (empty($this->contestants)) {
116
            return;
117
        }
118
119
        // recalc rank
120
        $this->reassignRank();
121
122
        foreach ($this->contestants as &$member) {
123
            $member["seed"]=1.0;
124
            foreach ($this->contestants as $other) {
125
                if ($member["uid"]!=$other["uid"]) {
126
                    $member["seed"]+=$this->getEloWinProbability($other["rating"], $member["rating"]);
127
                }
128
            }
129
        }
130
        unset($member);
131
132
        foreach ($this->contestants as &$contestant) {
133
            $midRank=sqrt($contestant["rank"] * $contestant["seed"]);
134
            $contestant["needRating"]=$this->getRatingToRank($midRank);
135
            $contestant["delta"]=floor(($contestant["needRating"]-$contestant["rating"]) / 2);
136
        }
137
        unset($contestant);
138
139
        $this->sort("rating");
140
141
        // DO some adjuct
142
        // Total sum should not be more than ZERO.
143
        $sum=0;
144
145
        foreach ($this->contestants as $contestant) {
146
            $sum+=$contestant["delta"];
147
        }
148
        $inc=-floor($sum / $this->totParticipants)-1;
149
        foreach ($this->contestants as &$contestant) {
150
            $contestant["delta"]+=$inc;
151
        }
152
        unset($contestant);
153
154
        // Sum of top-4*sqrt should be adjusted to ZERO.
155
156
        $sum=0;
157
        $zeroSumCount=min(intval(4 * round(sqrt($this->totParticipants))), $this->totParticipants);
158
159
        for ($i=0; $i<$zeroSumCount; $i++) {
160
            $sum+=$this->contestants[$i]["delta"];
161
        }
162
163
        $inc=min(max(-floor($sum / $zeroSumCount), -10), 0);
164
165
        for ($i=0; $i<$zeroSumCount; $i++) {
166
            $this->contestants[$i]["delta"]+=$inc;
167
        }
168
169
        return $this->validateDeltas();
170
    }
171
172
    public function storage() {
173
        $contestants=$this->contestants;
174
        DB::transaction(function() use ($contestants) {
175
            foreach ($contestants as $contestant) {
176
                $newRating=$contestant["rating"]+$contestant["delta"];
177
                DB::table("group_member")->where([
178
                    'gid' => $this->gid,
179
                    'uid' => $contestant['uid'],
180
                ])->update([
181
                    "ranking"=>$newRating
182
                ]);
183
                DB::table("group_rated_change_log")->insert([
184
                    'gid' => $this->gid,
185
                    "uid" => $contestant["uid"],
186
                    "cid" => $this->cid,
187
                    "ranking" => $newRating
188
                ]);
189
            }
190
        }, 5);
191
    }
192
193
    private function validateDeltas() {
194
        $this->sort("points");
195
196
        for ($i=0; $i<$this->totParticipants; $i++) {
197
            for ($j=$i+1; $j<$this->totParticipants; $j++) {
198
                if ($this->contestants[$i]["rating"]>$this->contestants[$j]["rating"]) {
199
                    if ($this->contestants[$i]["rating"]+$this->contestants[$i]["delta"]<$this->contestants[$j]["rating"]+$this->contestants[$j]["delta"]) {
200
                        Log::warning("First rating invariant failed: {$this->contestants[$i]["uid"]} vs. {$this->contestants[$j]["uid"]}.");
201
                        return false;
202
                    }
203
                }
204
205
                if ($this->contestants[$i]["rating"]<$this->contestants[$j]["rating"]) {
206
                    if ($this->contestants[$i]["delta"]<$this->contestants[$j]["delta"]) {
207
                        Log::warning("Second rating invariant failed: {$this->contestants[$i]["uid"]} vs.  {$this->contestants[$j]["uid"]}.");
208
                        return false;
209
                    }
210
                }
211
            }
212
        }
213
        return true;
214
    }
215
216
}
217