Completed
Push — master ( 82d2a4...865445 )
by John
27s queued 11s
created

GroupRatingCalculator::getRecord()   B

Complexity

Conditions 6
Paths 12

Size

Total Lines 26
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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