Completed
Push — feature/player-elo ( 88ae6a...994bf4 )
by Vladimir
11:06
created

Match::getPlayerEloDiff()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
/**
3
 * This file contains functionality relating to the official matches played in the league
4
 *
5
 * @package    BZiON\Models
6
 * @license    https://github.com/allejo/bzion/blob/master/LICENSE.md GNU General Public License Version 3
7
 */
8
use BZIon\Model\Column\Timestamp;
9
10
/**
11
 * A match played between two teams
12
 * @package    BZiON\Models
13
 */
14
class Match extends UrlModel implements NamedModel
15
{
16
    const OFFICIAL = "official";
17
    const SPECIAL  = "special";
18
    const FUN      = "fm";
19
20
    use Timestamp;
21
22
    /**
23
     * The ID of the first team of the match
24
     * @var int
25
     */
26
    protected $team_a;
27
28
    /**
29
     * The ID of the second team of the match
30
     * @var int
31
     */
32
    protected $team_b;
33
34
    /**
35
     * The color of the first team
36
     * @var string
37
     */
38
    protected $team_a_color;
39
40
    /**
41
     * The color of the second team
42
     * @var string
43
     */
44
    protected $team_b_color;
45
46
    /**
47
     * The match points (usually the number of flag captures) Team A scored
48
     * @var int
49
     */
50
    protected $team_a_points;
51
52
    /**
53
     * The match points Team B scored
54
     * @var int
55
     */
56
    protected $team_b_points;
57
58
    /**
59
     * The BZIDs of players part of Team A who participated in the match, separated by commas
60
     * @var string
61
     */
62
    protected $team_a_players;
63
64
    /**
65
     * The BZIDs of players part of Team B who participated in the match, separated by commas
66
     * @var string
67
     */
68
    protected $team_b_players;
69
70
    /**
71
     * The ELO score of Team A after the match
72
     * @var int
73
     */
74
    protected $team_a_elo_new;
75
76
    /**
77
     * The ELO score of Team B after the match
78
     * @var int
79
     */
80
    protected $team_b_elo_new;
81
82
    /**
83
     * The map ID used in the match if the league supports more than one map
84
     * @var int
85
     */
86
    protected $map;
87
88
    /**
89
     * The type of match that occurred. Valid options: official, fm, special
90
     *
91
     * @var string
92
     */
93
    protected $match_type;
94
95
    /**
96
     * A JSON string of events that happened during a match, such as captures and substitutions
97
     * @var string
98
     */
99
    protected $match_details;
100
101
    /**
102
     * The server location of there the match took place
103
     * @var string
104
     */
105
    protected $server;
106
107
    /**
108
     * The file name of the replay file of the match
109
     * @var string
110
     */
111
    protected $replay_file;
112
113
    /**
114
     * The value of the ELO score difference
115
     * @var int
116
     */
117
    protected $elo_diff;
118
119
    /**
120
     * The value of the player Elo difference
121
     * @var int
122
     */
123
    protected $player_elo_diff;
124
125
    /**
126
     * The timestamp representing when the match information was last updated
127
     * @var TimeDate
128
     */
129
    protected $updated;
130
131
    /**
132
     * The duration of the match in minutes
133
     * @var int
134
     */
135
    protected $duration;
136
137
    /**
138
     * The ID of the person (i.e. referee) who last updated the match information
139
     * @var string
140
     */
141
    protected $entered_by;
142
143
    /**
144
     * The status of the match. Can be 'entered', 'disabled', 'deleted' or 'reported'
145
     * @var string
146
     */
147
    protected $status;
148
149
    /**
150
     * The name of the database table used for queries
151
     */
152
    const TABLE = "matches";
153
154
    const CREATE_PERMISSION = Permission::ENTER_MATCH;
155
    const EDIT_PERMISSION = Permission::EDIT_MATCH;
156
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_MATCH;
157
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_MATCH;
158
159
    /**
160
     * {@inheritdoc}
161
     */
162 14
    protected function assignResult($match)
163
    {
164 14
        $this->team_a = $match['team_a'];
165 14
        $this->team_b = $match['team_b'];
166 14
        $this->team_a_color = $match['team_a_color'];
167 14
        $this->team_b_color = $match['team_b_color'];
168 14
        $this->team_a_points = $match['team_a_points'];
169 14
        $this->team_b_points = $match['team_b_points'];
170 14
        $this->team_a_players = $match['team_a_players'];
171 14
        $this->team_b_players = $match['team_b_players'];
172 14
        $this->team_a_elo_new = $match['team_a_elo_new'];
173 14
        $this->team_b_elo_new = $match['team_b_elo_new'];
174 14
        $this->map = $match['map'];
175 14
        $this->match_type = $match['match_type'];
176 14
        $this->match_details = $match['match_details'];
177 14
        $this->server = $match['server'];
178 14
        $this->replay_file = $match['replay_file'];
179 14
        $this->elo_diff = $match['elo_diff'];
180 14
        $this->player_elo_diff = $match['player_elo_diff'];
181 14
        $this->timestamp = TimeDate::fromMysql($match['timestamp']);
182 14
        $this->updated = TimeDate::fromMysql($match['updated']);
183 14
        $this->duration = $match['duration'];
184 14
        $this->entered_by = $match['entered_by'];
185 14
        $this->status = $match['status'];
186 14
    }
187
188
    /**
189
     * Get the name of the route that shows the object
190
     * @param  string $action The route's suffix
191
     * @return string
192
     */
193 1
    public static function getRouteName($action = 'show')
194
    {
195 1
        return "match_$action";
196
    }
197
198
    /**
199
     * Get a one word description of a match relative to a team (i.e. win, loss, or draw)
200
     *
201
     * @param int|string|TeamInterface $teamID The team ID we want the noun for
202
     *
203
     * @return string Either "win", "loss", or "draw" relative to the team
204
     */
205 1
    public function getMatchDescription($teamID)
206
    {
207 1
        if ($this->getScore($teamID) > $this->getOpponentScore($teamID)) {
208 1
            return "win";
209 1
        } elseif ($this->getScore($teamID) < $this->getOpponentScore($teamID)) {
210 1
            return "loss";
211
        }
212
213 1
        return "tie";
214
    }
215
216
    /**
217
     * Get a one letter description of a match relative to a team (i.e. W, L, or T)
218
     *
219
     * @param int|string|TeamInterface $teamID The team ID we want the noun for
220
     *
221
     * @return string Either "W", "L", or "T" relative to the team
222
     */
223 1
    public function getMatchLetter($teamID)
224
    {
225 1
        return strtoupper(substr($this->getMatchDescription($teamID), 0, 1));
226
    }
227
228
    /**
229
     * Get the score of a specific team
230
     *
231
     * @param int|string|TeamInterface $teamID The team we want the score for
232
     *
233
     * @return int The score that team received
234
     */
235 3 View Code Duplication
    public function getScore($teamID)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
236
    {
237 3
        if ($teamID instanceof TeamInterface) {
238
            // Oh no! The caller gave us a Team model instead of an ID!
239 3
            $teamID = $teamID->getId();
240 3
        } elseif (is_string($teamID)) {
241
            // Make sure we're comparing lowercase strings
242
            $teamID = strtolower($teamID);
243
        }
244
245 3
        if ($this->getTeamA()->getId() == $teamID) {
246 3
            return $this->getTeamAPoints();
247
        }
248
249 3
        return $this->getTeamBPoints();
250
    }
251
252
    /**
253
     * Get the score of the opponent relative to a team
254
     *
255
     * @param int|string|TeamInterface $teamID The opponent of the team we want the score for
256
     *
257
     * @return int The score of the opponent
258
     */
259 2
    public function getOpponentScore($teamID)
260
    {
261 2
        return $this->getScore($this->getOpponent($teamID));
262
    }
263
264
    /**
265
     * Get the opponent of a match relative to a team ID
266
     *
267
     * @param int|string|TeamInterface $teamID The team who is known in a match
268
     *
269
     * @return TeamInterface The opponent team
270
     */
271 12 View Code Duplication
    public function getOpponent($teamID)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
272
    {
273 12
        if ($teamID instanceof TeamInterface) {
274 12
            $teamID = $teamID->getId();
275 12
        } elseif (is_string($teamID)) {
276
            $teamID = strtolower($teamID);
277
        }
278
279 12
        if ($this->getTeamA()->getId() == $teamID) {
280 9
            return $this->getTeamB();
281
        }
282
283 5
        return $this->getTeamA();
284
    }
285
286
    /**
287
     * Get the timestamp of the last update of the match
288
     *
289
     * @return TimeDate The match's update timestamp
290
     */
291 1
    public function getUpdated()
292
    {
293 1
        return $this->updated->copy();
294
    }
295
296
    /**
297
     * Set the timestamp of the match
298
     *
299
     * @param  mixed $timestamp The match's new timestamp
300
     * @return $this
301
     */
302
    public function setTimestamp($timestamp)
303
    {
304
        $this->updateProperty($this->timestamp, "timestamp", TimeDate::from($timestamp));
305
306
        return $this;
307
    }
308
309
    /**
310
     * Get the first team involved in the match
311
     * @return TeamInterface Team A
312
     */
313 14
    public function getTeamA()
314
    {
315 14
        $team = Team::get($this->team_a);
316
317 14
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
318 12
            return $team;
319
        }
320
321 3
        return new ColorTeam($this->team_a_color);
322
    }
323
324
    /**
325
     * Get the second team involved in the match
326
     * @return TeamInterface Team B
327
     */
328 13
    public function getTeamB()
329
    {
330 13
        $team = Team::get($this->team_b);
331
332 13
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
333 11
            return $team;
334
        }
335
336 3
        return new ColorTeam($this->team_b_color);
337
    }
338
339
    /**
340
     * Get the color of Team A
341
     * @return string
342
     */
343
    public function getTeamAColor()
344
    {
345
        return $this->team_a_color;
346
    }
347
348
    /**
349
     * Get the color of Team B
350
     * @return string
351
     */
352
    public function getTeamBColor()
353
    {
354
        return $this->team_b_color;
355
    }
356
357
    /**
358
     * Get the list of players on Team A who participated in this match
359
     * @return Player[]|null Returns null if there were no players recorded for this match
360
     */
361 2
    public function getTeamAPlayers()
362
    {
363 2
        return $this->parsePlayers($this->team_a_players);
364
    }
365
366
    /**
367
     * Get the list of players on Team B who participated in this match
368
     * @return Player[]|null Returns null if there were no players recorded for this match
369
     */
370 1
    public function getTeamBPlayers()
371
    {
372 1
        return $this->parsePlayers($this->team_b_players);
373
    }
374
375
    /**
376
     * Get the list of players for a team in a match
377
     * @param  Team|int|null The team or team ID
378
     * @return Player[]|null Returns null if there were no players recorded for this match
379
     */
380 14
    public function getPlayers($team = null)
381
    {
382 14
        if ($team instanceof TeamInterface) {
383 1
            $team = $team->getId();
384 1
        }
385
386 14
        if ($this->getTeamA()->isValid() && $team == $this->getTeamA()->getId()) {
387 2
            return $this->getTeamAPlayers();
388 13
        } elseif ($this->getTeamB()->isValid() && $team == $this->getTeamB()->getId()) {
389 1
            return $this->getTeamBPlayers();
390
        }
391
392 13
        return $this->parsePlayers($this->team_a_players . "," . $this->team_b_players);
393
    }
394
395
    /**
396
     * Set the players of the match's teams
397
     *
398
     * @param int[] $teamAPlayers An array of player IDs
399
     * @param int[] $teamBPlayers An array of player IDs
400
     * @return self
401
     */
402
    public function setTeamPlayers($teamAPlayers = array(), $teamBPlayers = array())
403
    {
404
        $this->updateProperty($this->team_a_players, "team_a_players", implode(',', $teamAPlayers));
405
        $this->updateProperty($this->team_b_players, "team_b_players", implode(',', $teamBPlayers));
406
407
        return $this;
408
    }
409
410
    /**
411
     * Get an array of players based on a string representation
412
     * @param string $playerString
413
     * @return Player[]|null Returns null if there were no players recorded for this match
414
     */
415 14
    private function parsePlayers($playerString)
416
    {
417 14
        if ($playerString == null) {
418 1
            return null;
419
        }
420
421 14
        return Player::arrayIdToModel(explode(",", $playerString));
422
    }
423
424
    /**
425
     * Get the first team's points
426
     * @return int Team A's points
427
     */
428 5
    public function getTeamAPoints()
429
    {
430 5
        return $this->team_a_points;
431
    }
432
433
    /**
434
     * Get the second team's points
435
     * @return int Team B's points
436
     */
437 5
    public function getTeamBPoints()
438
    {
439 5
        return $this->team_b_points;
440
    }
441
442
    /**
443
     * Set the match team points
444
     *
445
     * @param  int $teamAPoints Team A's points
446
     * @param  int $teamBPoints Team B's points
447
     * @return self
448
     */
449
    public function setTeamPoints($teamAPoints, $teamBPoints)
450
    {
451
        $this->updateProperty($this->team_a_points, "team_a_points", $teamAPoints);
452
        $this->updateProperty($this->team_b_points, "team_b_points", $teamBPoints);
453
454
        return $this;
455
    }
456
457
    /**
458
     * Set the match team colors
459
     *
460
     * @param  ColorTeam|string $teamAColor The color of team A
461
     * @param  ColorTeam|string $teamBColor The color of team B
462
     * @return self
463
     */
464
    public function setTeamColors($teamAColor, $teamBColor)
465
    {
466
        if ($this->isOfficial()) {
467
            throw new \Exception("Cannot change team colors in an official match");
468
        }
469
470
        if ($teamAColor instanceof ColorTeam) {
471
            $teamAColor = $teamAColor->getId();
472
        }
473
        if ($teamBColor instanceof ColorTeam) {
474
            $teamBColor = $teamBColor->getId();
475
        }
476
477
        $this->updateProperty($this->team_a_color, "team_a_color", $teamAColor);
478
        $this->updateProperty($this->team_b_color, "team_b_color", $teamBColor);
479
    }
480
481
    /**
482
     * Get the ELO difference applied to each team's old ELO
483
     * @return int The ELO difference
484
     */
485 8
    public function getEloDiff()
486
    {
487 8
        return abs($this->elo_diff);
488
    }
489
490
    /**
491
     * Get the Elo difference applied to players
492
     *
493
     * @return int The Elo difference for players
494
     */
495 1
    public function getPlayerEloDiff()
496
    {
497 1
        return abs($this->player_elo_diff);
498
    }
499
500
    /**
501
     * Get the first team's new ELO
502
     * @return int Team A's new ELO
503
     */
504 7
    public function getTeamAEloNew()
505
    {
506 7
        return $this->team_a_elo_new;
507
    }
508
509
    /**
510
     * Get the second team's new ELO
511
     * @return int Team B's new ELO
512
     */
513 7
    public function getTeamBEloNew()
514
    {
515 7
        return $this->team_b_elo_new;
516
    }
517
518
    /**
519
     * Get the first team's old ELO
520
     * @return int
521
     */
522 6
    public function getTeamAEloOld()
523
    {
524 6
        return $this->team_a_elo_new - $this->elo_diff;
525
    }
526
527
    /**
528
     * Get the second team's old ELO
529
     * @return int
530
     */
531 6
    public function getTeamBEloOld()
532
    {
533 6
        return $this->team_b_elo_new + $this->elo_diff;
534
    }
535
536
    /**
537
     * Get the team's new ELO
538
     * @param  Team $team The team whose new ELO to return
539
     * @return int|null   The new ELO, or null if the team provided has not
540
     *                    participated in the match
541
     */
542 1
    public function getTeamEloNew(Team $team)
543
    {
544 1
        if ($team->getId() == $this->team_a) {
545 1
            return $this->getTeamAEloNew();
546 1
        } elseif ($team->getId() == $this->team_b) {
547 1
            return $this->getTeamBEloNew();
548
        }
549
550
        return null;
551
    }
552
553
    /**
554
     * Get the team's old ELO
555
     * @param  Team $team The team whose old ELO to return
556
     * @return int|null   The old ELO, or null if the team provided has not
557
     *                    participated in the match
558
     */
559 1
    public function getTeamEloOld(Team $team)
560
    {
561 1
        if ($team->getId() == $this->team_a) {
562 1
            return $this->getTeamAEloOld();
563 1
        } elseif ($team->getId() == $this->team_b) {
564 1
            return $this->getTeamBEloOld();
565
        }
566
567
        return null;
568
    }
569
570
    /**
571
     * Get the map where the match was played on
572
     * @return Map Returns an invalid map if no map was found
573
     */
574 1
    public function getMap()
575
    {
576 1
        return Map::get($this->map);
577
    }
578
579
    /**
580
     * Set the map where the match was played
581
     * @param  int $map The ID of the map
582
     * @return self
583
     */
584
    public function setMap($map)
585
    {
586
        $this->updateProperty($this->map, "map", $map, "s");
587
    }
588
589
    /**
590
     * Get the match type
591
     *
592
     * @return string 'official', 'fm', or 'special'
593
     */
594 14
    public function getMatchType()
595
    {
596 14
        return $this->match_type;
597
    }
598
599
    /**
600
     * Set the match type
601
     *
602
     * @param  string $matchType A valid match type; official, fm, special
603
     *
604
     * @return static
605
     */
606
    public function setMatchType($matchType)
607
    {
608
        return $this->updateProperty($this->match_type, "match_type", $matchType, 's');
609
    }
610
611
    /**
612
     * Get a JSON decoded array of events that occurred during the match
613
     * @return mixed|null Returns null if there were no events recorded for the match
614
     */
615
    public function getMatchDetails()
616
    {
617
        return json_decode($this->match_details);
618
    }
619
620
    /**
621
     * Get the server address of the server where this match took place
622
     * @return string|null Returns null if there was no server address recorded
623
     */
624 1
    public function getServerAddress()
625
    {
626 1
        return $this->server;
627
    }
628
629
    /**
630
     * Set the server address of the server where this match took place
631
     *
632
     * @param  string|null $server The server hostname
633
     * @param  int|null    $port   The server port
0 ignored issues
show
Bug introduced by
There is no parameter named $port. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
634
     * @return self
635
     */
636
    public function setServerAddress($server = null)
637
    {
638
        $this->updateProperty($this->server, "server", $server);
639
640
        return $this;
641
    }
642
643
    /**
644
     * Get the name of the replay file for this specific map
645
     * @param  int    $length The length of the replay file name; it will be truncated
646
     * @return string Returns null if there was no replay file name recorded
647
     */
648 1
    public function getReplayFileName($length = 0)
649
    {
650 1
        if ($length > 0) {
651
            return substr($this->replay_file, 0, $length);
652
        }
653
654 1
        return $this->replay_file;
655
    }
656
657
    /**
658
     * Get the match duration
659
     * @return int The duration of the match in minutes
660
     */
661 3
    public function getDuration()
662
    {
663 3
        return $this->duration;
664
    }
665
666
    /**
667
     * Set the match duration
668
     *
669
     * @param  int  $duration The new duration of the match in minutes
670
     * @return self
671
     */
672
    public function setDuration($duration)
673
    {
674
        return $this->updateProperty($this->duration, "duration", $duration);
675
    }
676
677
    /**
678
     * Get the user who entered the match
679
     * @return Player
680
     */
681 2
    public function getEnteredBy()
682
    {
683 2
        return Player::get($this->entered_by);
684
    }
685
686
    /**
687
     * Get the loser of the match
688
     *
689
     * @return TeamInterface The team that was the loser or the team with the lower elo if the match was a draw
690
     */
691 12
    public function getLoser()
692
    {
693
        // Get the winner of the match
694 12
        $winner = $this->getWinner();
695
696
        // Get the team that wasn't the winner... Duh
697 12
        return $this->getOpponent($winner);
698
    }
699
700
    /**
701
     * Get the winner of a match
702
     *
703
     * @return TeamInterface The team that was the victor or the team with the lower elo if the match was a draw
704
     */
705 12
    public function getWinner()
706
    {
707
        // Get the team that had its ELO increased
708 12
        if ($this->elo_diff > 0) {
709 9
            return $this->getTeamA();
710 4
        } elseif ($this->elo_diff < 0) {
711 3
            return $this->getTeamB();
712 1
        } elseif ($this->team_a_points > $this->team_b_points) {
713
            // In case we're dealing with a match such an FM that doesn't have an ELO difference
714 1
            return $this->getTeamA();
715
        } elseif ($this->team_a_points < $this->team_b_points) {
716
            return $this->getTeamB();
717
        }
718
719
        // If the scores are the same, return Team A because well, fuck you that's why
720
        return $this->getTeamA();
721
    }
722
723
    /**
724
     * Determine whether the match was a draw
725
     * @return bool True if the match ended without any winning teams
726
     */
727 13
    public function isDraw()
728
    {
729 13
        return $this->team_a_points == $this->team_b_points;
730
    }
731
732
    /**
733
     * Find out whether the match involves a team
734
     *
735
     * @param  TeamInterface $team The team to check
736
     * @return bool
737
     */
738 1
    public function involvesTeam($team)
739
    {
740 1
        return $team->getId() == $this->getTeamA()->getId() || $team->getId() == $this->getTeamB()->getId();
741
    }
742
743
    /**
744
     * Find out if the match is played between official teams
745
     */
746 14
    public function isOfficial()
747
    {
748 14
        return self::OFFICIAL === $this->getMatchType();
749
    }
750
751
    /**
752
     * Reset the ELOs of the teams participating in the match
753
     *
754
     * @return self
755
     */
756
    public function resetELOs()
757
    {
758
        if ($this->match_type === self::OFFICIAL) {
759
            $this->getTeamA()->changeELO(-$this->elo_diff);
0 ignored issues
show
Bug introduced by
The method changeELO() does not seem to exist on object<TeamInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
760
            $this->getTeamB()->changeELO(+$this->elo_diff);
0 ignored issues
show
Bug introduced by
The method changeELO() does not seem to exist on object<TeamInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
761
        }
762
763
        return $this;
764
    }
765
766
    /**
767
     * Calculate the match's contribution to the team activity
768
     *
769
     * @return float
770
     */
771 1
    public function getActivity()
772
    {
773 1
        $daysPassed = $this->getTimestamp()->diffInSeconds();
774 1
        $daysPassed = $daysPassed / TimeDate::SECONDS_PER_MINUTE / TimeDate::MINUTES_PER_HOUR / TimeDate::HOURS_PER_DAY;
775
776 1
        $activity = 0.0116687059537612 * (pow(45 - $daysPassed, (1 / 6)) + atan(31.0 - $daysPassed) / 2.0);
777
778 1
        if (is_nan($activity) || $activity < 0.0) {
779
            return 0.0;
780
        }
781
782 1
        return $activity;
783
    }
784
785
    /**
786
     * Enter a new match to the database
787
     * @param  int             $a          Team A's ID
788
     * @param  int             $b          Team B's ID
789
     * @param  int             $a_points   Team A's match points
790
     * @param  int             $b_points   Team B's match points
791
     * @param  int             $duration   The match duration in minutes
792
     * @param  int|null        $entered_by The ID of the player reporting the match
793
     * @param  string|DateTime $timestamp  When the match was played
794
     * @param  int[]           $a_players  The IDs of the first team's players
795
     * @param  int[]           $b_players  The IDs of the second team's players
796
     * @param  string|null     $server     The address of the server where the match was played
797
     * @param  int|null        $port       The port of the server where the match was played
0 ignored issues
show
Bug introduced by
There is no parameter named $port. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
798
     * @param  string          $replayFile The name of the replay file of the match
799
     * @param  int             $map        The ID of the map where the match was played, only for rotational leagues
800
     * @param  string          $matchType  The type of match (e.g. official, fm, special)
801
     * @param  string          $a_color    Team A's color
802
     * @param  string          $b_color    Team b's color
803
     * @return Match           An object representing the match that was just entered
804
     */
805 14
    public static function enterMatch(
806
        $a, $b, $a_points, $b_points, $duration, $entered_by, $timestamp = "now",
807
        $a_players = array(), $b_players = array(), $server = null, $replayFile = null,
808
        $map = null, $matchType = "official", $a_color = null, $b_color = null
809
    ) {
810
        $matchData = array(
811 14
            'team_a_color'   => strtolower($a_color),
812 14
            'team_b_color'   => strtolower($b_color),
813 14
            'team_a_points'  => $a_points,
814 14
            'team_b_points'  => $b_points,
815 14
            'team_a_players' => implode(',', $a_players),
816 14
            'team_b_players' => implode(',', $b_players),
817 14
            'timestamp'      => TimeDate::from($timestamp)->toMysql(),
818 14
            'duration'       => $duration,
819 14
            'entered_by'     => $entered_by,
820 14
            'server'         => $server,
821 14
            'replay_file'    => $replayFile,
822 14
            'map'            => $map,
823 14
            'status'         => 'entered',
824
            'match_type'     => $matchType
825 14
        );
826
827 14
        $playerEloDiff = null;
828
829 14
        if ($matchType === self::OFFICIAL) {
830 13
            $team_a = Team::get($a);
831 13
            $team_b = Team::get($b);
832
833 13
            $a_players_elo = null;
834 13
            $b_players_elo = null;
835
836
            // Only bother if we have players reported for both teams
837 13
            if (!empty($a_players) && !empty($b_players)) {
838 4
                $a_players_elo = self::getAveragePlayerElo($a_players);
839 4
                $b_players_elo = self::getAveragePlayerElo($b_players);
840
841 4
                $playerEloDiff = self::calculateEloDiff($a_players_elo, $b_players_elo, $a_points, $b_points, $duration);
842 4
            }
843
844
            // Get team ELOs, if not default to the average ELO of the players on the respective team
845 13
            $a_team_elo = ($team_a->isValid()) ? $team_a->getElo() : $a_players_elo;
846 13
            $b_team_elo = ($team_b->isValid()) ? $team_b->getElo() : $b_players_elo;
847
848 13
            if ($a_team_elo === null || $b_team_elo === null) {
849
                throw new Exception('An ELO for each team must be calculated somehow.');
850
            }
851
852 13
            $teamEloDiff = self::calculateEloDiff($a_team_elo, $b_team_elo, $a_points, $b_points, $duration);
853
854 13
            $matchData['elo_diff'] = $teamEloDiff;
855 13
            $matchData['player_elo_diff'] = $playerEloDiff;
856
857
            // Update team ELOs
858 13
            if ($team_a->isValid()) {
859 12
                $team_a->adjustElo($teamEloDiff);
860
861 12
                $matchData['team_a'] = $a;
862 12
                $matchData['team_a_elo_new'] = $team_a->getElo();
863 12
            }
864 13
            if ($team_b->isValid()) {
865 11
                $team_b->adjustElo(-$teamEloDiff);
866
867 11
                $matchData['team_b'] = $b;
868 11
                $matchData['team_b_elo_new'] = $team_b->getElo();
869 11
            }
870 13
        }
871
872 14
        $match = self::create($matchData, 'updated');
873 14
        $match->updateMatchCount();
874
875 14
        $players = $match->getPlayers();
876
877 14
        $db = Database::getInstance();
878 14
        $db->startTransaction();
879
880
        /** @var Player $player */
881 14
        foreach ($players as $player) {
0 ignored issues
show
Bug introduced by
The expression $players of type null|array<integer,object<Model>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
882 14
            $diff = $playerEloDiff;
883
884 14
            if ($playerEloDiff !== null && !in_array($player->getId(), $a_players)) {
885 4
                $diff = -$playerEloDiff;
886 4
            }
887
888 14
            $player->setMatchParticipation($match, $diff);
889 14
        }
890
891 14
        $db->finishTransaction();
892
893 14
        return $match;
894
    }
895
896
    /**
897
     * Calculate the ELO score difference
898
     *
899
     * Computes the ELO score difference on each team after a match, based on
900
     * GU League's rules.
901
     *
902
     * @param  int $a_elo    Team A's current ELO score
903
     * @param  int $b_elo    Team B's current ELO score
904
     * @param  int $a_points Team A's match points
905
     * @param  int $b_points Team B's match points
906
     * @param  int $duration The match duration in minutes
907
     * @return int The ELO score difference
908
     */
909 13
    public static function calculateEloDiff($a_elo, $b_elo, $a_points, $b_points, $duration)
910
    {
911 13
        $prob = 1.0 / (1 + pow(10, (($b_elo - $a_elo) / 400.0)));
912 13
        if ($a_points > $b_points) {
913 8
            $diff = 50 * (1 - $prob);
914 13
        } elseif ($a_points == $b_points) {
915 4
            $diff = 50 * (0.5 - $prob);
916 4
        } else {
917 2
            $diff = 50 * (0 - $prob);
918
        }
919
920
        // Apply ELO modifiers from `config.yml`
921 13
        $durations = Service::getParameter('bzion.league.duration');
922 13
        $diff *= (isset($durations[$duration])) ? $durations[$duration] : 1;
923
924 13
        if (abs($diff) < 1 && $diff != 0) {
925
            // ELOs such as 0.75 should round up to 1...
926 2
            return ($diff > 0) ? 1 : -1;
927
        }
928
929
        // ...everything else is rounded down (-3.7 becomes -3 and 48.1 becomes 48)
930 11
        return intval($diff);
931
    }
932
933
    /**
934
     * Find if a match's stored ELO is correct
935
     */
936
    public function isEloCorrect()
937
    {
938
        return $this->elo_diff === $this->calculateEloDiff(
939
            $this->getTeamAEloOld(),
940
            $this->getTeamBEloOld(),
941
            $this->getTeamAPoints(),
942
            $this->getTeamBPoints(),
943
            $this->getDuration()
944
        );
945
    }
946
947
    /**
948
     * Recalculate the match's elo and adjust the team ELO values
949
     */
950
    public function recalculateElo()
951
    {
952
        if ($this->match_type !== self::OFFICIAL) {
953
            return;
954
        }
955
956
        $a = $this->getTeamA();
957
        $b = $this->getTeamB();
958
959
        $elo = $this->calculateEloDiff(
960
            $a->getElo(),
961
            $b->getElo(),
962
            $this->getTeamAPoints(),
963
            $this->getTeamBPoints(),
964
            $this->getDuration()
965
        );
966
967
        $this->updateProperty($this->elo_diff, "elo_diff", $elo);
968
969
        $a->changeElo($elo);
0 ignored issues
show
Bug introduced by
The method changeElo() does not seem to exist on object<TeamInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
970
        $b->changeElo(-$elo);
0 ignored issues
show
Bug introduced by
The method changeElo() does not seem to exist on object<TeamInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
971
972
        $this->updateProperty($this->team_a_elo_new, "team_a_elo_new", $a->getElo());
973
        $this->updateProperty($this->team_b_elo_new, "team_b_elo_new", $b->getElo());
974
    }
975
976
    /**
977
     * Get all the matches in the database
978
     */
979 1
    public static function getMatches()
980
    {
981 1
        return self::getQueryBuilder()->active()->getModels();
982
    }
983
984
    /**
985
     * Get a query builder for matches
986
     * @return MatchQueryBuilder
987
     */
988 4
    public static function getQueryBuilder()
989
    {
990 4
        return new MatchQueryBuilder('Match', array(
991
            'columns' => array(
992 4
                'firstTeam'        => 'team_a',
993 4
                'secondTeam'       => 'team_b',
994 4
                'firstTeamPoints'  => 'team_a_points',
995 4
                'secondTeamPoints' => 'team_b_points',
996 4
                'time'             => 'timestamp',
997 4
                'map'              => 'map',
998 4
                'type'             => 'match_type',
999
                'status'           => 'status'
1000 4
            ),
1001 4
        ));
1002
    }
1003
1004
    /**
1005
     * {@inheritdoc}
1006
     */
1007
    public function delete()
1008
    {
1009
        $this->updateMatchCount(true);
1010
1011
        parent::delete();
1012
    }
1013
1014
    /**
1015
     * {@inheritdoc}
1016
     */
1017 3
    public static function getActiveStatuses()
1018
    {
1019 3
        return array('entered');
1020
    }
1021
1022
    /**
1023
     * {@inheritdoc}
1024
     */
1025 2
    public function getName()
1026
    {
1027 2
        switch ($this->getMatchType()) {
1028 2
            case self::OFFICIAL:
1029 2
                $description = "(+/- " . $this->getEloDiff() . ")";
1030 2
                break;
1031 1
            case self::FUN:
1032 1
                $description = "Fun Match:";
1033 1
                break;
1034
            case self::SPECIAL:
1035
                $description = "Special Match:";
1036
                break;
1037
            default:
1038
                $description = "";
1039 2
        }
1040
1041 2
        return sprintf("%s %s [%d] vs [%d] %s",
1042 2
            $description,
1043 2
            $this->getWinner()->getName(),
1044 2
            $this->getScore($this->getWinner()),
1045 2
            $this->getScore($this->getLoser()),
1046 2
            $this->getLoser()->getName()
1047 2
        );
1048
    }
1049
1050
    /**
1051
     * Get the average ELO for an array of players
1052
     *
1053
     * @param int[] $players
1054
     *
1055
     * @return float|int
1056
     */
1057
    private static function getAveragePlayerElo($players)
1058
    {
1059 4
        $getElo = function ($n) {
1060 4
            return Player::get($n)->getElo();
1061 4
        };
1062
1063 4
        return array_sum(array_map($getElo, $players)) / count($players);
1064
    }
1065
1066
    /**
1067
     * Update the match count of the teams participating in the match
1068
     *
1069
     * @param bool $decrement Whether to decrement instead of incrementing the match count
1070
     */
1071 14
    private function updateMatchCount($decrement = false)
1072
    {
1073 14
        if ($this->match_type !== self::OFFICIAL) {
1074 2
            return;
1075
        }
1076
1077 13
        $diff = ($decrement) ? -1 : 1;
1078
1079 13
        if ($this->isDraw()) {
1080 4
            $this->getTeamA()->supportsMatchCount() && $this->getTeamA()->changeMatchCount($diff, 'draw');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method changeMatchCount() does only exist in the following implementations of said interface: Team.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1081 4
            $this->getTeamB()->supportsMatchCount() && $this->getTeamB()->changeMatchCount($diff, 'draw');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method changeMatchCount() does only exist in the following implementations of said interface: Team.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1082 4
        } else {
1083 10
            $this->getWinner()->supportsMatchCount() && $this->getWinner()->changeMatchCount($diff, 'win');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method changeMatchCount() does only exist in the following implementations of said interface: Team.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1084 10
            $this->getLoser()->supportsMatchCount()  && $this->getLoser()->changeMatchCount($diff, 'loss');
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method changeMatchCount() does only exist in the following implementations of said interface: Team.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
1085
        }
1086 13
    }
1087
}
1088