Completed
Push — feature/player-elo ( b75a11...062671 )
by Vladimir
03:05
created

Match::enterMatch()   D

Complexity

Conditions 15
Paths 52

Size

Total Lines 89
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 49
CRAP Score 15

Importance

Changes 0
Metric Value
dl 0
loc 89
ccs 49
cts 49
cp 1
rs 4.9121
c 0
b 0
f 0
cc 15
eloc 55
nc 52
nop 15
crap 15

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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
    const TEAM_V_TEAM   = 0;
21
    const TEAM_V_MIXED  = 1;
22
    const MIXED_V_MIXED = 2;
23
24
    use Timestamp;
25
26
    /**
27
     * The ID of the first team of the match
28
     * @var int
29
     */
30
    protected $team_a;
31
32
    /**
33
     * The ID of the second team of the match
34
     * @var int
35
     */
36
    protected $team_b;
37
38
    /**
39
     * The color of the first team
40
     * @var string
41
     */
42
    protected $team_a_color;
43
44
    /**
45
     * The color of the second team
46
     * @var string
47
     */
48
    protected $team_b_color;
49
50
    /**
51
     * The match points (usually the number of flag captures) Team A scored
52
     * @var int
53
     */
54
    protected $team_a_points;
55
56
    /**
57
     * The match points Team B scored
58
     * @var int
59
     */
60
    protected $team_b_points;
61
62
    /**
63
     * The BZIDs of players part of Team A who participated in the match, separated by commas
64
     * @var string
65
     */
66
    protected $team_a_players;
67
68
    /**
69
     * The BZIDs of players part of Team B who participated in the match, separated by commas
70
     * @var string
71
     */
72
    protected $team_b_players;
73
74
    /**
75
     * The ELO score of Team A after the match
76
     * @var int
77
     */
78
    protected $team_a_elo_new;
79
80
    /**
81
     * The ELO score of Team B after the match
82
     * @var int
83
     */
84
    protected $team_b_elo_new;
85
86
    /**
87
     * The map ID used in the match if the league supports more than one map
88
     * @var int
89
     */
90
    protected $map;
91
92
    /**
93
     * The type of match that occurred. Valid options: official, fm, special
94
     *
95
     * @var string
96
     */
97
    protected $match_type;
98
99
    /**
100
     * A JSON string of events that happened during a match, such as captures and substitutions
101
     * @var string
102
     */
103
    protected $match_details;
104
105
    /**
106
     * The server location of there the match took place
107
     * @var string
108
     */
109
    protected $server;
110
111
    /**
112
     * The file name of the replay file of the match
113
     * @var string
114
     */
115
    protected $replay_file;
116
117
    /**
118
     * The value of the ELO score difference
119
     * @var int
120
     */
121
    protected $elo_diff;
122
123
    /**
124
     * The value of the player Elo difference
125
     * @var int
126
     */
127
    protected $player_elo_diff;
128
129
    /**
130
     * The timestamp representing when the match information was last updated
131
     * @var TimeDate
132
     */
133
    protected $updated;
134
135
    /**
136
     * The duration of the match in minutes
137
     * @var int
138
     */
139
    protected $duration;
140
141
    /**
142
     * The ID of the person (i.e. referee) who last updated the match information
143
     * @var string
144
     */
145
    protected $entered_by;
146
147
    /**
148
     * The status of the match. Can be 'entered', 'disabled', 'deleted' or 'reported'
149
     * @var string
150
     */
151
    protected $status;
152
153
    /**
154
     * The name of the database table used for queries
155
     */
156
    const TABLE = "matches";
157
158
    const CREATE_PERMISSION = Permission::ENTER_MATCH;
159
    const EDIT_PERMISSION = Permission::EDIT_MATCH;
160
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_MATCH;
161
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_MATCH;
162
163
    /**
164
     * {@inheritdoc}
165
     */
166 34
    protected function assignResult($match)
167
    {
168 34
        $this->team_a = $match['team_a'];
169 34
        $this->team_b = $match['team_b'];
170 34
        $this->team_a_color = $match['team_a_color'];
171 34
        $this->team_b_color = $match['team_b_color'];
172 34
        $this->team_a_points = $match['team_a_points'];
173 34
        $this->team_b_points = $match['team_b_points'];
174 34
        $this->team_a_players = $match['team_a_players'];
175 34
        $this->team_b_players = $match['team_b_players'];
176 34
        $this->team_a_elo_new = $match['team_a_elo_new'];
177 34
        $this->team_b_elo_new = $match['team_b_elo_new'];
178 34
        $this->map = $match['map'];
179 34
        $this->match_type = $match['match_type'];
180 34
        $this->match_details = $match['match_details'];
181 34
        $this->server = $match['server'];
182 34
        $this->replay_file = $match['replay_file'];
183 34
        $this->elo_diff = $match['elo_diff'];
184 34
        $this->player_elo_diff = $match['player_elo_diff'];
185 34
        $this->timestamp = TimeDate::fromMysql($match['timestamp']);
186 34
        $this->updated = TimeDate::fromMysql($match['updated']);
187 34
        $this->duration = $match['duration'];
188 34
        $this->entered_by = $match['entered_by'];
189 34
        $this->status = $match['status'];
190 34
    }
191
192
    /**
193
     * Get the name of the route that shows the object
194
     * @param  string $action The route's suffix
195
     * @return string
196
     */
197 1
    public static function getRouteName($action = 'show')
198
    {
199 1
        return "match_$action";
200
    }
201
202
    /**
203
     * Get a one word description of a match relative to a team (i.e. win, loss, or draw)
204
     *
205
     * @param int|string|TeamInterface $teamID The team ID we want the noun for
206
     *
207
     * @return string Either "win", "loss", or "draw" relative to the team
208
     */
209 1
    public function getMatchDescription($teamID)
210
    {
211 1
        if ($this->getScore($teamID) > $this->getOpponentScore($teamID)) {
212 1
            return "win";
213 1
        } elseif ($this->getScore($teamID) < $this->getOpponentScore($teamID)) {
214 1
            return "loss";
215
        }
216
217 1
        return "tie";
218
    }
219
220
    /**
221
     * Get a one letter description of a match relative to a team (i.e. W, L, or T)
222
     *
223
     * @param int|string|TeamInterface $teamID The team ID we want the noun for
224
     *
225
     * @return string Either "W", "L", or "T" relative to the team
226
     */
227 1
    public function getMatchLetter($teamID)
228
    {
229 1
        return strtoupper(substr($this->getMatchDescription($teamID), 0, 1));
230
    }
231
232
    /**
233
     * Get the score of a specific team
234
     *
235
     * @param int|string|TeamInterface $teamID The team we want the score for
236
     *
237
     * @return int The score that team received
238
     */
239 23 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...
240
    {
241 23
        if ($teamID instanceof TeamInterface) {
242
            // Oh no! The caller gave us a Team model instead of an ID!
243 23
            $teamID = $teamID->getId();
244 2
        } elseif (is_string($teamID)) {
245
            // Make sure we're comparing lowercase strings
246
            $teamID = strtolower($teamID);
247
        }
248
249 23
        if ($this->getTeamA()->getId() == $teamID) {
250 23
            return $this->getTeamAPoints();
251
        }
252
253 23
        return $this->getTeamBPoints();
254
    }
255
256
    /**
257
     * Get the score of the opponent relative to a team
258
     *
259
     * @param int|string|TeamInterface $teamID The opponent of the team we want the score for
260
     *
261
     * @return int The score of the opponent
262
     */
263 2
    public function getOpponentScore($teamID)
264
    {
265 2
        return $this->getScore($this->getOpponent($teamID));
266
    }
267
268
    /**
269
     * Get the opponent of a match relative to a team ID
270
     *
271
     * @param int|string|TeamInterface $teamID The team who is known in a match
272
     *
273
     * @return TeamInterface The opponent team
274
     */
275 32 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...
276
    {
277 32
        if ($teamID instanceof TeamInterface) {
278 32
            $teamID = $teamID->getId();
279 2
        } elseif (is_string($teamID)) {
280
            $teamID = strtolower($teamID);
281
        }
282
283 32
        if ($this->getTeamA()->getId() == $teamID) {
284 21
            return $this->getTeamB();
285
        }
286
287 13
        return $this->getTeamA();
288
    }
289
290
    /**
291
     * Get the timestamp of the last update of the match
292
     *
293
     * @return TimeDate The match's update timestamp
294
     */
295 1
    public function getUpdated()
296
    {
297 1
        return $this->updated->copy();
298
    }
299
300
    /**
301
     * Set the timestamp of the match
302
     *
303
     * @param  mixed $timestamp The match's new timestamp
304
     * @return $this
305
     */
306
    public function setTimestamp($timestamp)
307
    {
308
        $this->updateProperty($this->timestamp, "timestamp", TimeDate::from($timestamp));
309
310
        return $this;
311
    }
312
313
    /**
314
     * Get the first team involved in the match
315
     * @return TeamInterface Team A
316
     */
317 34
    public function getTeamA()
318
    {
319 34
        $team = Team::get($this->team_a);
320
321 34
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
322 23
            return $team;
323
        }
324
325 12
        return new ColorTeam($this->team_a_color);
326
    }
327
328
    /**
329
     * Get the second team involved in the match
330
     * @return TeamInterface Team B
331
     */
332 33
    public function getTeamB()
333
    {
334 33
        $team = Team::get($this->team_b);
335
336 33
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
337 22
            return $team;
338
        }
339
340 12
        return new ColorTeam($this->team_b_color);
341
    }
342
343
    /**
344
     * Get the color of Team A
345
     * @return string
346
     */
347
    public function getTeamAColor()
348
    {
349
        return $this->team_a_color;
350
    }
351
352
    /**
353
     * Get the color of Team B
354
     * @return string
355
     */
356
    public function getTeamBColor()
357
    {
358
        return $this->team_b_color;
359
    }
360
361
    /**
362
     * Get the list of players on Team A who participated in this match
363
     * @return Player[] Returns null if there were no players recorded for this match
364
     */
365 2
    public function getTeamAPlayers()
366
    {
367 2
        return $this->parsePlayers($this->team_a_players);
368
    }
369
370
    /**
371
     * Get the list of players on Team B who participated in this match
372
     * @return Player[] Returns null if there were no players recorded for this match
373
     */
374 1
    public function getTeamBPlayers()
375
    {
376 1
        return $this->parsePlayers($this->team_b_players);
377
    }
378
379
    /**
380
     * Get the list of players for a team in a match
381
     * @param  Team|int|null The team or team ID
382
     * @return Player[]|null Returns null if there were no players recorded for this match
383
     */
384 34
    public function getPlayers($team = null)
385
    {
386 34
        if ($team instanceof TeamInterface) {
387 1
            $team = $team->getId();
388
        }
389
390 34
        if ($this->getTeamA()->isValid() && $team == $this->getTeamA()->getId()) {
391 2
            return $this->getTeamAPlayers();
392 33
        } elseif ($this->getTeamB()->isValid() && $team == $this->getTeamB()->getId()) {
393 1
            return $this->getTeamBPlayers();
394
        }
395
396 33
        return $this->parsePlayers($this->team_a_players . "," . $this->team_b_players);
397
    }
398
399
    /**
400
     * Set the players of the match's teams
401
     *
402
     * @param int[] $teamAPlayers An array of player IDs
403
     * @param int[] $teamBPlayers An array of player IDs
404
     * @return self
405
     */
406
    public function setTeamPlayers($teamAPlayers = array(), $teamBPlayers = array())
407
    {
408
        $this->updateProperty($this->team_a_players, "team_a_players", implode(',', $teamAPlayers));
409
        $this->updateProperty($this->team_b_players, "team_b_players", implode(',', $teamBPlayers));
410
411
        return $this;
412
    }
413
414
    /**
415
     * Get an array of players based on a string representation
416
     * @param string $playerString
417
     * @return Player[] Returns null if there were no players recorded for this match
418
     */
419 34
    private function parsePlayers($playerString)
420
    {
421 34
        if ($playerString == null) {
422 1
            return [];
423
        }
424
425 34
        return Player::arrayIdToModel(explode(",", $playerString));
426
    }
427
428
    /**
429
     * Get the first team's points
430
     * @return int Team A's points
431
     */
432 25
    public function getTeamAPoints()
433
    {
434 25
        return $this->team_a_points;
435
    }
436
437
    /**
438
     * Get the second team's points
439
     * @return int Team B's points
440
     */
441 25
    public function getTeamBPoints()
442
    {
443 25
        return $this->team_b_points;
444
    }
445
446
    /**
447
     * Set the match team points
448
     *
449
     * @param  int $teamAPoints Team A's points
450
     * @param  int $teamBPoints Team B's points
451
     * @return self
452
     */
453
    public function setTeamPoints($teamAPoints, $teamBPoints)
454
    {
455
        $this->updateProperty($this->team_a_points, "team_a_points", $teamAPoints);
456
        $this->updateProperty($this->team_b_points, "team_b_points", $teamBPoints);
457
458
        return $this;
459
    }
460
461
    /**
462
     * Set the match team colors
463
     *
464
     * @param  ColorTeam|string $teamAColor The color of team A
465
     * @param  ColorTeam|string $teamBColor The color of team B
466
     * @return self
467
     */
468
    public function setTeamColors($teamAColor, $teamBColor)
469
    {
470
        if ($this->isOfficial()) {
471
            throw new \Exception("Cannot change team colors in an official match");
472
        }
473
474
        if ($teamAColor instanceof ColorTeam) {
475
            $teamAColor = $teamAColor->getId();
476
        }
477
        if ($teamBColor instanceof ColorTeam) {
478
            $teamBColor = $teamBColor->getId();
479
        }
480
481
        $this->updateProperty($this->team_a_color, "team_a_color", $teamAColor);
482
        $this->updateProperty($this->team_b_color, "team_b_color", $teamBColor);
483
    }
484
485
    /**
486
     * Get the ELO difference applied to each team's old ELO
487
     *
488
     * @param bool $absoluteValue Whether or not to get the absolute value of the Elo difference
489
     *
490
     * @return int The ELO difference
491
     */
492 28
    public function getEloDiff($absoluteValue = true)
493
    {
494 28
        return ($absoluteValue) ? abs($this->elo_diff) : $this->elo_diff;
495
    }
496
497
    /**
498
     * Get the Elo difference applied to players
499
     *
500
     * @param bool $absoluteValue Whether or not to get the absolute value of the Elo difference
501
     *
502
     * @return int The Elo difference for players
503
     */
504 20
    public function getPlayerEloDiff($absoluteValue = true)
505
    {
506 20
        return ($absoluteValue) ? abs($this->player_elo_diff) : $this->player_elo_diff;
507
    }
508
509
    /**
510
     * Get the first team's new ELO
511
     * @return int Team A's new ELO
512
     */
513 7
    public function getTeamAEloNew()
514
    {
515 7
        return $this->team_a_elo_new;
516
    }
517
518
    /**
519
     * Get the second team's new ELO
520
     * @return int Team B's new ELO
521
     */
522 7
    public function getTeamBEloNew()
523
    {
524 7
        return $this->team_b_elo_new;
525
    }
526
527
    /**
528
     * Get the first team's old ELO
529
     * @return int
530
     */
531 6
    public function getTeamAEloOld()
532
    {
533 6
        return $this->team_a_elo_new - $this->elo_diff;
534
    }
535
536
    /**
537
     * Get the second team's old ELO
538
     * @return int
539
     */
540 6
    public function getTeamBEloOld()
541
    {
542 6
        return $this->team_b_elo_new + $this->elo_diff;
543
    }
544
545
    /**
546
     * Get the team's new ELO
547
     * @param  Team $team The team whose new ELO to return
548
     * @return int|null   The new ELO, or null if the team provided has not
549
     *                    participated in the match
550
     */
551 1
    public function getTeamEloNew(Team $team)
552
    {
553 1
        if ($team->getId() == $this->team_a) {
554 1
            return $this->getTeamAEloNew();
555 1
        } elseif ($team->getId() == $this->team_b) {
556 1
            return $this->getTeamBEloNew();
557
        }
558
559
        return null;
560
    }
561
562
    /**
563
     * Get the team's old ELO
564
     * @param  Team $team The team whose old ELO to return
565
     * @return int|null   The old ELO, or null if the team provided has not
566
     *                    participated in the match
567
     */
568 1
    public function getTeamEloOld(Team $team)
569
    {
570 1
        if ($team->getId() == $this->team_a) {
571 1
            return $this->getTeamAEloOld();
572 1
        } elseif ($team->getId() == $this->team_b) {
573 1
            return $this->getTeamBEloOld();
574
        }
575
576
        return null;
577
    }
578
579
    /**
580
     * Get the map where the match was played on
581
     * @return Map Returns an invalid map if no map was found
582
     */
583 1
    public function getMap()
584
    {
585 1
        return Map::get($this->map);
586
    }
587
588
    /**
589
     * Set the map where the match was played
590
     * @param  int $map The ID of the map
591
     * @return self
592
     */
593
    public function setMap($map)
594
    {
595
        $this->updateProperty($this->map, "map", $map, "s");
596
    }
597
598
    /**
599
     * Get the type of official match this is. Whether it has just traditional teams or has mixed teams.
600
     *
601
     * Possible official match types:
602
     *   - Team vs Team
603
     *   - Team vs Mixed
604
     *   - Mixed vs Mixed
605
     *
606
     * @see Match::TEAM_V_TEAM
607
     * @see Match::TEAM_V_MIXED
608
     * @see Match::MIXED_V_MIXED
609
     *
610
     * @return int
611
     */
612 20
    public function getTeamMatchType()
613
    {
614 20
        if ($this->getTeamA()->supportsMatchCount() && $this->getTeamB()->supportsMatchCount()) {
615 8
            return self::TEAM_V_TEAM;
616 12
        } elseif ($this->getTeamA()->supportsMatchCount() xor $this->getTeamB()->supportsMatchCount()) {
617 12
            return self::TEAM_V_MIXED;
618
        }
619
620
        return self::MIXED_V_MIXED;
621
    }
622
623
    /**
624
     * Get the match type
625
     *
626
     * @return string 'official', 'fm', or 'special'
627
     */
628 34
    public function getMatchType()
629
    {
630 34
        return $this->match_type;
631
    }
632
633
    /**
634
     * Set the match type
635
     *
636
     * @param  string $matchType A valid match type; official, fm, special
637
     *
638
     * @return static
639
     */
640
    public function setMatchType($matchType)
641
    {
642
        return $this->updateProperty($this->match_type, "match_type", $matchType, 's');
643
    }
644
645
    /**
646
     * Get a JSON decoded array of events that occurred during the match
647
     * @return mixed|null Returns null if there were no events recorded for the match
648
     */
649
    public function getMatchDetails()
650
    {
651
        return json_decode($this->match_details);
652
    }
653
654
    /**
655
     * Get the server address of the server where this match took place
656
     * @return string|null Returns null if there was no server address recorded
657
     */
658 1
    public function getServerAddress()
659
    {
660 1
        return $this->server;
661
    }
662
663
    /**
664
     * Set the server address of the server where this match took place
665
     *
666
     * @param  string|null $server The server hostname
667
     * @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...
668
     * @return self
669
     */
670
    public function setServerAddress($server = null)
671
    {
672
        $this->updateProperty($this->server, "server", $server);
673
674
        return $this;
675
    }
676
677
    /**
678
     * Get the name of the replay file for this specific map
679
     * @param  int    $length The length of the replay file name; it will be truncated
680
     * @return string Returns null if there was no replay file name recorded
681
     */
682 1
    public function getReplayFileName($length = 0)
683
    {
684 1
        if ($length > 0) {
685
            return substr($this->replay_file, 0, $length);
686
        }
687
688 1
        return $this->replay_file;
689
    }
690
691
    /**
692
     * Get the match duration
693
     * @return int The duration of the match in minutes
694
     */
695 3
    public function getDuration()
696
    {
697 3
        return $this->duration;
698
    }
699
700
    /**
701
     * Set the match duration
702
     *
703
     * @param  int  $duration The new duration of the match in minutes
704
     * @return self
705
     */
706
    public function setDuration($duration)
707
    {
708
        return $this->updateProperty($this->duration, "duration", $duration);
709
    }
710
711
    /**
712
     * Get the user who entered the match
713
     * @return Player
714
     */
715 2
    public function getEnteredBy()
716
    {
717 2
        return Player::get($this->entered_by);
718
    }
719
720
    /**
721
     * Get the loser of the match
722
     *
723
     * @return TeamInterface The team that was the loser or the team with the lower elo if the match was a draw
724
     */
725 32
    public function getLoser()
726
    {
727
        // Get the winner of the match
728 32
        $winner = $this->getWinner();
729
730
        // Get the team that wasn't the winner... Duh
731 32
        return $this->getOpponent($winner);
732
    }
733
734
    /**
735
     * Get the winner of a match
736
     *
737
     * @return TeamInterface The team that was the victor or the team with the lower elo if the match was a draw
738
     */
739 32
    public function getWinner()
740
    {
741
        // "As Mother Teresa once said, it's not enough if you win. Others must lose."
742
        //   -Stephen Colbert
743
744
        // Get the team that had its Elo increased or the team whose players had their Elo increased
745 32
        if ($this->elo_diff > 0 || $this->player_elo_diff > 0) {
746 19
            return $this->getTeamA();
747 14
        } elseif ($this->elo_diff < 0 || $this->player_elo_diff < 0) {
748 11
            return $this->getTeamB();
749 3
        } elseif ($this->team_a_points > $this->team_b_points) {
750
            // In case we're dealing with a match such an FM that doesn't have an ELO difference
751 2
            return $this->getTeamA();
752 1
        } elseif ($this->team_a_points < $this->team_b_points) {
753
            return $this->getTeamB();
754
        }
755
756
        // If the scores are the same, return Team A because well, fuck you that's why
757 1
        return $this->getTeamA();
758
    }
759
760
    /**
761
     * Determine whether the match was a draw
762
     * @return bool True if the match ended without any winning teams
763
     */
764 32
    public function isDraw()
765
    {
766 32
        return $this->team_a_points == $this->team_b_points;
767
    }
768
769
    /**
770
     * Find out whether the match involves a team
771
     *
772
     * @param  TeamInterface $team The team to check
773
     * @return bool
774
     */
775 1
    public function involvesTeam($team)
776
    {
777 1
        return $team->getId() == $this->getTeamA()->getId() || $team->getId() == $this->getTeamB()->getId();
778
    }
779
780
    /**
781
     * Find out if the match is played between official teams
782
     */
783 34
    public function isOfficial()
784
    {
785 34
        return self::OFFICIAL === $this->getMatchType();
786
    }
787
788
    /**
789
     * Reset the ELOs of the teams participating in the match
790
     *
791
     * @return self
792
     */
793
    public function resetELOs()
794
    {
795
        if ($this->match_type === self::OFFICIAL) {
796
            $this->getTeamA()->supportsMatchCount() && $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...
797
            $this->getTeamB()->supportsMatchCount() && $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...
798
        }
799
800
        return $this;
801
    }
802
803
    /**
804
     * Calculate the match's contribution to the team activity
805
     *
806
     * @return float
807
     */
808 1
    public function getActivity()
809
    {
810 1
        $daysPassed = $this->getTimestamp()->diffInSeconds();
811 1
        $daysPassed = $daysPassed / TimeDate::SECONDS_PER_MINUTE / TimeDate::MINUTES_PER_HOUR / TimeDate::HOURS_PER_DAY;
812
813 1
        $activity = 0.0116687059537612 * (pow(45 - $daysPassed, (1 / 6)) + atan(31.0 - $daysPassed) / 2.0);
814
815 1
        if (is_nan($activity) || $activity < 0.0) {
816
            return 0.0;
817
        }
818
819 1
        return $activity;
820
    }
821
822
    /**
823
     * Enter a new match to the database
824
     * @param  int             $a          Team A's ID
825
     * @param  int             $b          Team B's ID
826
     * @param  int             $a_points   Team A's match points
827
     * @param  int             $b_points   Team B's match points
828
     * @param  int             $duration   The match duration in minutes
829
     * @param  int|null        $entered_by The ID of the player reporting the match
830
     * @param  string|DateTime $timestamp  When the match was played
831
     * @param  int[]           $a_players  The IDs of the first team's players
832
     * @param  int[]           $b_players  The IDs of the second team's players
833
     * @param  string|null     $server     The address of the server where the match was played
834
     * @param  string          $replayFile The name of the replay file of the match
835
     * @param  int             $map        The ID of the map where the match was played, only for rotational leagues
836
     * @param  string          $matchType  The type of match (e.g. official, fm, special)
837
     * @param  string          $a_color    Team A's color
838
     * @param  string          $b_color    Team b's color
839
     *
840
     * @throws InvalidArgumentException When a ColorTeam is selected for an official match and no players are defined
841
     *                                  for that team
842 34
     *
843
     * @return Match           An object representing the match that was just entered
844
     */
845
    public static function enterMatch(
846
        $a, $b, $a_points, $b_points, $duration, $entered_by, $timestamp = "now",
847
        $a_players = array(), $b_players = array(), $server = null, $replayFile = null,
848 34
        $map = null, $matchType = "official", $a_color = null, $b_color = null
849 34
    ) {
850 34
        $matchData = array(
851 34
            'team_a_color'   => strtolower($a_color),
852 34
            'team_b_color'   => strtolower($b_color),
853 34
            'team_a_points'  => $a_points,
854 34
            'team_b_points'  => $b_points,
855 34
            'team_a_players' => implode(',', $a_players),
856 34
            'team_b_players' => implode(',', $b_players),
857 34
            'timestamp'      => TimeDate::from($timestamp)->toMysql(),
858 34
            'duration'       => $duration,
859 34
            'entered_by'     => $entered_by,
860 34
            'server'         => $server,
861 34
            'replay_file'    => $replayFile,
862
            'map'            => $map,
863
            'status'         => 'entered',
864 34
            'match_type'     => $matchType
865
        );
866 34
867 32
        $playerEloDiff = null;
868 32
869
        if ($matchType === self::OFFICIAL) {
870 32
            $team_a = Team::get($a);
871 32
            $team_b = Team::get($b);
872
873
            if ((!$team_a->isValid() && empty($a_players)) ||
874 32
                (!$team_b->isValid() && empty($b_players))) {
875 23
                throw new InvalidArgumentException('A color team must have a player roster to calculate the player Elo');
876 23
            }
877
878 23
            // Only bother if we have players reported for both teams
879
            if (!empty($a_players) && !empty($b_players)) {
880
                $a_players_elo = self::getAveragePlayerElo($a_players);
881
                $b_players_elo = self::getAveragePlayerElo($b_players);
882
883 32
                $playerEloDiff = self::calculateEloDiff($a_players_elo, $b_players_elo, $a_points, $b_points, $duration);
884
            }
885 32
886 18
            // If it's a Team vs Team official match, we need to calculate the Elo diff between the two teams. Otherwise,
887
            // we'll be using the player Elo diff as the "team" Elo diff for future calculations and database persistence
888
            $teamEloDiff = $playerEloDiff;
889 32
            if ($team_a->isValid() && $team_b->isValid()) {
890 32
                $teamEloDiff = self::calculateEloDiff($team_a->getElo(), $team_b->getElo(), $a_points, $b_points, $duration);
891
            }
892
893 32
            $matchData['elo_diff'] = $teamEloDiff;
894 23
            $matchData['player_elo_diff'] = $playerEloDiff;
895
896 23
            // Update team ELOs
897 23
            if ($team_a->isValid()) {
898
                $team_a->adjustElo($teamEloDiff);
899 32
900 22
                $matchData['team_a'] = $a;
901
                $matchData['team_a_elo_new'] = $team_a->getElo();
902 22
            }
903 22
            if ($team_b->isValid()) {
904
                $team_b->adjustElo(-$teamEloDiff);
905
906
                $matchData['team_b'] = $b;
907 34
                $matchData['team_b_elo_new'] = $team_b->getElo();
908 34
            }
909
        }
910 34
911
        $match = self::create($matchData, 'updated');
912 34
        $match->updateMatchCount();
913 34
914
        $players = $match->getPlayers();
915
916 34
        $db = Database::getInstance();
917 34
        $db->startTransaction();
918
919 34
        /** @var Player $player */
920 23
        foreach ($players as $player) {
0 ignored issues
show
Bug introduced by
The expression $players of type array<integer,object<Player>>|null 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...
921
            $diff = $playerEloDiff;
922
923 34
            if ($playerEloDiff !== null && !in_array($player->getId(), $a_players)) {
924
                $diff = -$playerEloDiff;
925
            }
926 34
927
            $player->setMatchParticipation($match, $diff);
928 34
        }
929
930
        $db->finishTransaction();
931
932
        return $match;
933
    }
934
935
    /**
936
     * Calculate the ELO score difference
937
     *
938
     * Computes the ELO score difference on each team after a match, based on
939
     * GU League's rules.
940
     *
941
     * @param  int $a_elo    Team A's current ELO score
942
     * @param  int $b_elo    Team B's current ELO score
943
     * @param  int $a_points Team A's match points
944 32
     * @param  int $b_points Team B's match points
945
     * @param  int $duration The match duration in minutes
946 32
     * @return int The ELO score difference
947 32
     */
948 11
    public static function calculateEloDiff($a_elo, $b_elo, $a_points, $b_points, $duration)
949 22
    {
950 16
        $prob = 1.0 / (1 + pow(10, (($b_elo - $a_elo) / 400.0)));
951
        if ($a_points > $b_points) {
952 6
            $diff = 50 * (1 - $prob);
953
        } elseif ($a_points == $b_points) {
954
            $diff = 50 * (0.5 - $prob);
955
        } else {
956 32
            $diff = 50 * (0 - $prob);
957 32
        }
958
959 32
        // Apply ELO modifiers from `config.yml`
960
        $durations = Service::getParameter('bzion.league.duration');
961 2
        $diff *= (isset($durations[$duration])) ? $durations[$duration] : 1;
962
963
        if (abs($diff) < 1 && $diff != 0) {
964
            // ELOs such as 0.75 should round up to 1...
965 30
            return ($diff > 0) ? 1 : -1;
966
        }
967
968
        // ...everything else is rounded down (-3.7 becomes -3 and 48.1 becomes 48)
969
        return intval($diff);
970
    }
971
972
    /**
973
     * Find if a match's stored ELO is correct
974
     */
975
    public function isEloCorrect()
976
    {
977
        return $this->elo_diff === $this->calculateEloDiff(
978
            $this->getTeamAEloOld(),
979
            $this->getTeamBEloOld(),
980
            $this->getTeamAPoints(),
981
            $this->getTeamBPoints(),
982
            $this->getDuration()
983
        );
984
    }
985
986
    /**
987
     * Recalculate the match's elo and adjust the team ELO values
988
     */
989
    public function recalculateElo()
990
    {
991
        if ($this->match_type !== self::OFFICIAL) {
992
            return;
993
        }
994
995
        $a = $this->getTeamA();
996
        $b = $this->getTeamB();
997
998
        $elo = $this->calculateEloDiff(
999
            $a->getElo(),
1000
            $b->getElo(),
1001
            $this->getTeamAPoints(),
1002
            $this->getTeamBPoints(),
1003
            $this->getDuration()
1004
        );
1005
1006
        $this->updateProperty($this->elo_diff, "elo_diff", $elo);
1007
1008
        $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...
1009
        $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...
1010
1011
        $this->updateProperty($this->team_a_elo_new, "team_a_elo_new", $a->getElo());
1012
        $this->updateProperty($this->team_b_elo_new, "team_b_elo_new", $b->getElo());
1013
    }
1014 1
1015
    /**
1016 1
     * Get all the matches in the database
1017
     */
1018
    public static function getMatches()
1019
    {
1020
        return self::getQueryBuilder()->active()->getModels();
1021
    }
1022
1023 25
    /**
1024
     * Get a query builder for matches
1025 25
     * @return MatchQueryBuilder
1026 25
     */
1027
    public static function getQueryBuilder()
1028
    {
1029
        return new MatchQueryBuilder('Match', array(
1030
            'columns' => array(
1031
                'firstTeam'        => 'team_a',
1032
                'secondTeam'       => 'team_b',
1033
                'firstTeamPoints'  => 'team_a_points',
1034
                'secondTeamPoints' => 'team_b_points',
1035
                'time'             => 'timestamp',
1036
                'map'              => 'map',
1037
                'type'             => 'match_type',
1038
                'status'           => 'status'
1039
            ),
1040
        ));
1041
    }
1042
1043
    /**
1044
     * {@inheritdoc}
1045
     */
1046
    public function delete()
1047
    {
1048
        $this->updateMatchCount(true);
1049
1050
        parent::delete();
1051
    }
1052 3
1053
    /**
1054 3
     * {@inheritdoc}
1055
     */
1056
    public static function getActiveStatuses()
1057
    {
1058
        return array('entered');
1059
    }
1060 22
1061
    /**
1062 22
     * {@inheritdoc}
1063
     */
1064 22
    public function getName()
1065 22
    {
1066
        $description = '';
1067 21
1068 9
        switch ($this->getMatchType()) {
1069
            case self::OFFICIAL:
1070 21
                // Only show Elo diff if both teams are actual teams
1071
                if ($this->getTeamA()->supportsMatchCount() && $this->getTeamB()->supportsMatchCount()) {
1072 2
                    $description = "(+/- {$this->getEloDiff()})";
1073 2
                }
1074 2
                break;
1075
1076
            case self::FUN:
1077
                $description = 'Fun Match:';
1078
                break;
1079
1080
            case self::SPECIAL:
1081
                $description = 'Special Match:';
1082
                break;
1083
1084 22
            default:
1085 22
                break;
1086 22
        }
1087 22
1088 22
        return trim(sprintf('%s %s [%d] vs [%d] %s',
1089 22
            $description,
1090
            $this->getWinner()->getName(),
1091
            $this->getScore($this->getWinner()),
1092
            $this->getScore($this->getLoser()),
1093
            $this->getLoser()->getName()
1094
        ));
1095
    }
1096
1097
    /**
1098
     * Get the average ELO for an array of players
1099
     *
1100
     * @param int[] $players
1101
     *
1102 23
     * @return float|int
1103 23
     */
1104 23
    private static function getAveragePlayerElo($players)
1105
    {
1106 23
        $getElo = function ($n) {
1107
            return Player::get($n)->getElo();
1108
        };
1109
1110
        return array_sum(array_map($getElo, $players)) / count($players);
1111
    }
1112
1113
    /**
1114 34
     * Update the match count of the teams participating in the match
1115
     *
1116 34
     * @param bool $decrement Whether to decrement instead of incrementing the match count
1117 3
     */
1118
    private function updateMatchCount($decrement = false)
1119
    {
1120 32
        if ($this->match_type !== self::OFFICIAL) {
1121
            return;
1122 32
        }
1123 16
1124 16
        $diff = ($decrement) ? -1 : 1;
1125
1126 17
        if ($this->isDraw()) {
1127 17
            $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...
1128
            $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...
1129 32
        } else {
1130
            $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...
1131
            $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...
1132
        }
1133
    }
1134
}
1135