Completed
Push — feature/match-info-restructure ( cfa28e )
by Vladimir
15:22 queued 29s
created

Match::resetTeamElos()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 9
rs 9.2
c 0
b 0
f 0
cc 4
eloc 5
nc 5
nop 0
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 callsigns players used during this match.
64
     * @var string[]
65
     */
66
    protected $player_callsigns;
67
68
    /**
69
     * An associative array of IP addresses used by players during this match.
70
     * @var string[]
71
     */
72
    protected $player_ip_addresses;
73
74
    /**
75
     * The players who participated in Team A during this match.
76
     * @var Player[]
77
     */
78
    protected $team_a_players;
79
80
    /**
81
     * The players who participated in Team B during this match.
82
     * @var Player[]
83
     */
84
    protected $team_b_players;
85
86
    /**
87
     * The ELO score of Team A after the match
88
     * @var int
89
     */
90
    protected $team_a_elo_new;
91
92
    /**
93
     * The ELO score of Team B after the match
94
     * @var int
95
     */
96
    protected $team_b_elo_new;
97
98
    /**
99
     * The map ID used in the match if the league supports more than one map
100
     * @var int
101
     */
102
    protected $map;
103
104
    /**
105
     * The type of match that occurred. Valid options: official, fm, special
106
     *
107
     * @var string
108
     */
109
    protected $match_type;
110
111
    /**
112
     * A JSON string of events that happened during a match, such as captures and substitutions
113
     * @var string
114
     */
115
    protected $match_details;
116
117
    /**
118
     * The ID of the server where this match took place
119
     * @var int
120
     */
121
    protected $server;
122
123
    /**
124
     * The server location of there the match took place
125
     * @var string
126
     */
127
    protected $server_address;
128
129
    /**
130
     * The file name of the replay file of the match
131
     * @var string
132
     */
133
    protected $replay_file;
134
135
    /**
136
     * The value of the ELO score difference
137
     * @var int
138
     */
139
    protected $elo_diff;
140
141
    /**
142
     * The value of the player Elo difference
143
     * @var int
144
     */
145
    protected $player_elo_diff;
146
147
    /**
148
     * @var array
149
     */
150
    protected $player_elo_changelog;
151
152
    /**
153
     * The timestamp representing when the match information was last updated
154
     * @var TimeDate
155
     */
156
    protected $updated;
157
158
    /**
159
     * The duration of the match in minutes
160
     * @var int
161
     */
162
    protected $duration;
163
164
    /**
165
     * The ID of the person (i.e. referee) who last updated the match information
166
     * @var string
167
     */
168
    protected $entered_by;
169
170
    const DEFAULT_STATUS = 'entered';
171
172
    /**
173
     * The name of the database table used for queries
174
     */
175
    const TABLE = "matches";
176
177
    const CREATE_PERMISSION = Permission::ENTER_MATCH;
178
    const EDIT_PERMISSION = Permission::EDIT_MATCH;
179
    const SOFT_DELETE_PERMISSION = Permission::SOFT_DELETE_MATCH;
180
    const HARD_DELETE_PERMISSION = Permission::HARD_DELETE_MATCH;
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    protected function assignResult($match)
186
    {
187
        $this->team_a = $match['team_a'];
188
        $this->team_b = $match['team_b'];
189
        $this->team_a_color = $match['team_a_color'];
190
        $this->team_b_color = $match['team_b_color'];
191
        $this->team_a_points = $match['team_a_points'];
192
        $this->team_b_points = $match['team_b_points'];
193
        $this->team_a_elo_new = $match['team_a_elo_new'];
194
        $this->team_b_elo_new = $match['team_b_elo_new'];
195
        $this->map = $match['map'];
196
        $this->match_type = $match['match_type'];
197
        $this->match_details = $match['match_details'];
198
        $this->server_address = $match['server'];
199
        $this->replay_file = $match['replay_file'];
200
        $this->elo_diff = $match['elo_diff'];
201
        $this->player_elo_diff = $match['player_elo_diff'];
202
        $this->timestamp = TimeDate::fromMysql($match['timestamp']);
203
        $this->updated = TimeDate::fromMysql($match['updated']);
204
        $this->duration = $match['duration'];
205
        $this->entered_by = $match['entered_by'];
206
        $this->status = $match['status'];
207
208
        // For legacy support (e.g. Phinx migrations) we need to check if the key exists. Prior to migrations, it didn't
209
        // exist so Phinx migrations prior to 20170912201127_match_server_relationship will throw warnings.
210
        //
211
        // @todo Look for a better solution
212
        $this->server = isset($match['server_id']) ? $match['server_id'] : null;
213
    }
214
215
    /**
216
     * Get the name of the route that shows the object
217
     * @param  string $action The route's suffix
218
     * @return string
219
     */
220
    public static function getRouteName($action = 'show')
221
    {
222
        return "match_$action";
223
    }
224
225
    /**
226
     * Get a one word description of a match relative to a team (i.e. win, loss, or draw)
227
     *
228
     * @param int|string|TeamInterface $teamID The team ID we want the noun for
229
     *
230
     * @return string Either "win", "loss", or "draw" relative to the team
231
     */
232
    public function getMatchDescription($teamID)
233
    {
234
        if ($this->getScore($teamID) > $this->getOpponentScore($teamID)) {
235
            return "win";
236
        } elseif ($this->getScore($teamID) < $this->getOpponentScore($teamID)) {
237
            return "loss";
238
        }
239
240
        return "tie";
241
    }
242
243
    /**
244
     * Get a one letter description of a match relative to a team (i.e. W, L, or T)
245
     *
246
     * @param int|string|TeamInterface $teamID The team ID we want the noun for
247
     *
248
     * @return string Either "W", "L", or "T" relative to the team
249
     */
250
    public function getMatchLetter($teamID)
251
    {
252
        return strtoupper(substr($this->getMatchDescription($teamID), 0, 1));
253
    }
254
255
    /**
256
     * Get the score of a specific team
257
     *
258
     * @param int|string|TeamInterface $teamID The team we want the score for
259
     *
260
     * @return int The score that team received
261
     */
262 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...
263
    {
264
        if ($teamID instanceof TeamInterface) {
265
            // Oh no! The caller gave us a Team model instead of an ID!
266
            $teamID = $teamID->getId();
267
        } elseif (is_string($teamID)) {
268
            // Make sure we're comparing lowercase strings
269
            $teamID = strtolower($teamID);
270
        }
271
272
        if ($this->getTeamA()->getId() == $teamID) {
273
            return $this->getTeamAPoints();
274
        }
275
276
        return $this->getTeamBPoints();
277
    }
278
279
    /**
280
     * Get the score of the opponent relative to a team
281
     *
282
     * @param int|string|TeamInterface $teamID The opponent of the team we want the score for
283
     *
284
     * @return int The score of the opponent
285
     */
286
    public function getOpponentScore($teamID)
287
    {
288
        return $this->getScore($this->getOpponent($teamID));
289
    }
290
291
    /**
292
     * Get the opponent of a match relative to a team ID
293
     *
294
     * @param int|string|TeamInterface $teamID The team who is known in a match
295
     *
296
     * @return TeamInterface The opponent team
297
     */
298 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...
299
    {
300
        if ($teamID instanceof TeamInterface) {
301
            $teamID = $teamID->getId();
302
        } elseif (is_string($teamID)) {
303
            $teamID = strtolower($teamID);
304
        }
305
306
        if ($this->getTeamA()->getId() == $teamID) {
307
            return $this->getTeamB();
308
        }
309
310
        return $this->getTeamA();
311
    }
312
313
    /**
314
     * Get the timestamp of the last update of the match
315
     *
316
     * @return TimeDate The match's update timestamp
317
     */
318
    public function getUpdated()
319
    {
320
        return $this->updated->copy();
321
    }
322
323
    /**
324
     * Set the timestamp of the match
325
     *
326
     * @param  mixed $timestamp The match's new timestamp
327
     * @return $this
328
     */
329
    public function setTimestamp($timestamp)
330
    {
331
        $this->updateProperty($this->timestamp, "timestamp", TimeDate::from($timestamp));
332
333
        return $this;
334
    }
335
336
    /**
337
     * Get the first team involved in the match
338
     * @return TeamInterface Team A
339
     */
340
    public function getTeamA()
341
    {
342
        $team = Team::get($this->team_a);
343
344
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
345
            return $team;
346
        }
347
348
        return new ColorTeam($this->team_a_color);
349
    }
350
351
    /**
352
     * Get the second team involved in the match
353
     * @return TeamInterface Team B
354
     */
355
    public function getTeamB()
356
    {
357
        $team = Team::get($this->team_b);
358
359
        if ($this->match_type === self::OFFICIAL && $team->isValid()) {
360
            return $team;
361
        }
362
363
        return new ColorTeam($this->team_b_color);
364
    }
365
366
    /**
367
     * Get the color of Team A
368
     * @return string
369
     */
370
    public function getTeamAColor()
371
    {
372
        return $this->team_a_color;
373
    }
374
375
    /**
376
     * Get the color of Team B
377
     * @return string
378
     */
379
    public function getTeamBColor()
380
    {
381
        return $this->team_b_color;
382
    }
383
384
    /**
385
     * Get the IP address a player used during this match.
386
     *
387
     * @param Player|int $id
388
     *
389
     * @return string|null
390
     */
391
    public function getPlayerIpAddress($id)
392
    {
393
        $this->lazyLoadMatchParticipants();
394
395
        if ($id instanceof Player) {
396
            $id = $id->getId();
397
        }
398
399
        return __::get($this->player_ip_addresses, $id, null);
400
    }
401
402
    /**
403
     * Get the callsign a player used during this match.
404
     *
405
     * @param Player|int $id
406
     *
407
     * @return string|null
408
     */
409
    public function getPlayerCallsign($id)
410
    {
411
        $this->lazyLoadMatchParticipants();
412
413
        if ($id instanceof Player) {
414
            $id = $id->getId();
415
        }
416
417
        return __::get($this->player_callsigns, $id, null);
418
    }
419
420
    /**
421
     * Get the list of players on Team A who participated in this match
422
     * @return Player[] Returns null if there were no players recorded for this match
423
     */
424
    public function getTeamAPlayers()
425
    {
426
        $this->lazyLoadMatchParticipants();
427
428
        return $this->team_a_players;
429
    }
430
431
    /**
432
     * Get the list of players on Team B who participated in this match
433
     * @return Player[] Returns null if there were no players recorded for this match
434
     */
435
    public function getTeamBPlayers()
436
    {
437
        $this->lazyLoadMatchParticipants();
438
439
        return $this->team_b_players;
440
    }
441
442
    /**
443
     * Get the list of players for a team in a match
444
     * @param  Team|int|null The team or team ID
445
     * @return Player[]|null Returns null if there were no players recorded for this match
446
     */
447
    public function getPlayers($team = null)
448
    {
449
        if ($team instanceof TeamInterface) {
450
            $team = $team->getId();
451
        }
452
453
        if ($this->getTeamA()->isValid() && $team === $this->getTeamA()->getId()) {
454
            return $this->getTeamAPlayers();
455
        } elseif ($this->getTeamB()->isValid() && $team === $this->getTeamB()->getId()) {
456
            return $this->getTeamBPlayers();
457
        }
458
459
        return array_merge($this->getTeamAPlayers(), $this->getTeamBPlayers());
460
    }
461
462
    /**
463
     * Set the players of the match's teams
464
     *
465
     * @param int[] $teamAPlayers An array of player IDs
466
     * @param int[] $teamBPlayers An array of player IDs
467
     * @return self
468
     */
469
    public function setTeamPlayers($teamAPlayers = array(), $teamBPlayers = array())
0 ignored issues
show
Unused Code introduced by
The parameter $teamAPlayers is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $teamBPlayers is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
470
    {
471
        // @todo Update this to use the new table and add support for IPs and callsigns
472
        // $this->updateProperty($this->team_a_players, "team_a_players", implode(',', $teamAPlayers));
473
        // $this->updateProperty($this->team_b_players, "team_b_players", implode(',', $teamBPlayers));
474
475
        return $this;
476
    }
477
478
    /**
479
     * Load player participation in this match from its respective tables.
480
     */
481
    private function lazyLoadMatchParticipants()
482
    {
483
        if ($this->team_a_players !== null || $this->team_b_players !== null) {
484
            return;
485
        }
486
487
        $participation = $this->db->query('SELECT * FROM match_participation WHERE match_id = ?', [
488
            $this->getId(),
489
        ]);
490
491
        $this->team_a_players = Player::arrayIdToModel(__::where($participation, ['team_loyalty' => 0]));
0 ignored issues
show
Documentation Bug introduced by
It seems like \Player::arrayIdToModel(...('team_loyalty' => 0))) of type array<integer,object<Model>> is incompatible with the declared type array<integer,object<Player>> of property $team_a_players.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
492
        $this->team_b_players = Player::arrayIdToModel(__::where($participation, ['team_loyalty' => 1]));
0 ignored issues
show
Documentation Bug introduced by
It seems like \Player::arrayIdToModel(...('team_loyalty' => 1))) of type array<integer,object<Model>> is incompatible with the declared type array<integer,object<Player>> of property $team_b_players.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
493
494
        $user_ids = array_column($participation, 'user_id');
495
        $callsigns = array_column($participation, 'callsign');
496
        $ip_addresses = array_column($participation, 'ip_address');
497
498
        $this->player_callsigns = array_combine($user_ids, $callsigns);
499
        $this->player_ip_addresses = array_combine($user_ids, $ip_addresses);
500
    }
501
502
    /**
503
     * Get the first team's points
504
     * @return int Team A's points
505
     */
506
    public function getTeamAPoints()
507
    {
508
        return $this->team_a_points;
509
    }
510
511
    /**
512
     * Get the second team's points
513
     * @return int Team B's points
514
     */
515
    public function getTeamBPoints()
516
    {
517
        return $this->team_b_points;
518
    }
519
520
    /**
521
     * Set the match team points
522
     *
523
     * @param  int $teamAPoints Team A's points
524
     * @param  int $teamBPoints Team B's points
525
     * @return self
526
     */
527
    public function setTeamPoints($teamAPoints, $teamBPoints)
528
    {
529
        $this->updateProperty($this->team_a_points, "team_a_points", $teamAPoints);
530
        $this->updateProperty($this->team_b_points, "team_b_points", $teamBPoints);
531
532
        return $this;
533
    }
534
535
    /**
536
     * Set the match team colors
537
     *
538
     * @param  ColorTeam|string $teamAColor The color of team A
539
     * @param  ColorTeam|string $teamBColor The color of team B
540
     * @return self
541
     */
542
    public function setTeamColors($teamAColor, $teamBColor)
543
    {
544
        if ($this->isOfficial()) {
545
            throw new \Exception("Cannot change team colors in an official match");
546
        }
547
548
        if ($teamAColor instanceof ColorTeam) {
549
            $teamAColor = $teamAColor->getId();
550
        }
551
        if ($teamBColor instanceof ColorTeam) {
552
            $teamBColor = $teamBColor->getId();
553
        }
554
555
        $this->updateProperty($this->team_a_color, "team_a_color", $teamAColor);
556
        $this->updateProperty($this->team_b_color, "team_b_color", $teamBColor);
557
    }
558
559
    /**
560
     * Get the ELO difference applied to each team's old ELO
561
     *
562
     * @param bool $absoluteValue Whether or not to get the absolute value of the Elo difference
563
     *
564
     * @return int The ELO difference
565
     */
566
    public function getEloDiff($absoluteValue = true)
567
    {
568
        return ($absoluteValue) ? abs($this->elo_diff) : $this->elo_diff;
569
    }
570
571
    /**
572
     * Get the Elo difference applied to players
573
     *
574
     * @param bool $absoluteValue Whether or not to get the absolute value of the Elo difference
575
     *
576
     * @return int The Elo difference for players
577
     */
578
    public function getPlayerEloDiff($absoluteValue = true)
579
    {
580
        return ($absoluteValue && $this->player_elo_diff !== null) ? abs($this->player_elo_diff) : $this->player_elo_diff;
581
    }
582
583
    /**
584
     * Get the changelog for the player Elos for this match and cache them
585
     */
586
    private function getPlayerEloChangelog()
587
    {
588
        if ($this->player_elo_changelog !== null) {
589
            return;
590
        }
591
592
        $results = $this->db->query('SELECT * FROM player_elo WHERE match_id = ?', $this->getId());
593
594
        foreach ($results as $result) {
595
            $this->player_elo_changelog[$result['user_id']] = [
596
                'before' => $result['elo_previous'],
597
                'after'  => $result['elo_new']
598
            ];
599
        }
600
    }
601
602
    /**
603
     * Get the Elo for the player before this match occurred
604
     *
605
     * @param Player $player
606
     *
607
     * @return null|int
608
     */
609 View Code Duplication
    public function getPlayerEloBefore(Player $player)
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...
610
    {
611
        $this->getPlayerEloChangelog();
612
613
        if (isset($this->player_elo_changelog[$player->getId()])) {
614
            return $this->player_elo_changelog[$player->getId()]['before'];
615
        }
616
617
        return null;
618
    }
619
620
    /**
621
     * Get the Elo for the player after this match occurred
622
     *
623
     * @param Player $player
624
     *
625
     * @return null|int
626
     */
627 View Code Duplication
    public function getPlayerEloAfter(Player $player)
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...
628
    {
629
        $this->getPlayerEloChangelog();
630
631
        if (isset($this->player_elo_changelog[$player->getId()])) {
632
            return $this->player_elo_changelog[$player->getId()]['after'];
633
        }
634
635
        return null;
636
    }
637
638
    /**
639
     * Set the Elo difference applied to players
640
     *
641
     * @param int $diff
642
     */
643
    public function setPlayerEloDiff($diff)
644
    {
645
        $this->updateProperty($this->player_elo_diff, 'player_elo_diff', $diff);
646
    }
647
648
    /**
649
     * Get the first team's new ELO
650
     * @return int Team A's new ELO
651
     */
652
    public function getTeamAEloNew()
653
    {
654
        return $this->team_a_elo_new;
655
    }
656
657
    /**
658
     * Get the second team's new ELO
659
     * @return int Team B's new ELO
660
     */
661
    public function getTeamBEloNew()
662
    {
663
        return $this->team_b_elo_new;
664
    }
665
666
    /**
667
     * Get the first team's old ELO
668
     * @return int
669
     */
670
    public function getTeamAEloOld()
671
    {
672
        return $this->team_a_elo_new - $this->elo_diff;
673
    }
674
675
    /**
676
     * Get the second team's old ELO
677
     * @return int
678
     */
679
    public function getTeamBEloOld()
680
    {
681
        return $this->team_b_elo_new + $this->elo_diff;
682
    }
683
684
    /**
685
     * Get the team's new ELO
686
     * @param  Team $team The team whose new ELO to return
687
     * @return int|null   The new ELO, or null if the team provided has not
688
     *                    participated in the match
689
     */
690
    public function getTeamEloNew(Team $team)
691
    {
692
        if ($team->getId() == $this->team_a) {
693
            return $this->getTeamAEloNew();
694
        } elseif ($team->getId() == $this->team_b) {
695
            return $this->getTeamBEloNew();
696
        }
697
698
        return null;
699
    }
700
701
    /**
702
     * Get the team's old ELO
703
     * @param  Team $team The team whose old ELO to return
704
     * @return int|null   The old ELO, or null if the team provided has not
705
     *                    participated in the match
706
     */
707
    public function getTeamEloOld(Team $team)
708
    {
709
        if ($team->getId() == $this->team_a) {
710
            return $this->getTeamAEloOld();
711
        } elseif ($team->getId() == $this->team_b) {
712
            return $this->getTeamBEloOld();
713
        }
714
715
        return null;
716
    }
717
718
    /**
719
     * Get the map where the match was played on
720
     * @return Map Returns an invalid map if no map was found
721
     */
722
    public function getMap()
723
    {
724
        return Map::get($this->map);
725
    }
726
727
    /**
728
     * Set the map where the match was played
729
     * @param  int $map The ID of the map
730
     * @return self
731
     */
732
    public function setMap($map)
733
    {
734
        $this->updateProperty($this->map, "map", $map, "s");
735
736
        return $this;
737
    }
738
739
    /**
740
     * Get the type of official match this is. Whether it has just traditional teams or has mixed teams.
741
     *
742
     * Possible official match types:
743
     *   - Team vs Team
744
     *   - Team vs Mixed
745
     *   - Mixed vs Mixed
746
     *
747
     * @see Match::TEAM_V_TEAM
748
     * @see Match::TEAM_V_MIXED
749
     * @see Match::MIXED_V_MIXED
750
     *
751
     * @return int
752
     */
753
    public function getTeamMatchType()
754
    {
755
        if ($this->getTeamA()->supportsMatchCount() && $this->getTeamB()->supportsMatchCount()) {
756
            return self::TEAM_V_TEAM;
757
        } elseif ($this->getTeamA()->supportsMatchCount() xor $this->getTeamB()->supportsMatchCount()) {
758
            return self::TEAM_V_MIXED;
759
        }
760
761
        return self::MIXED_V_MIXED;
762
    }
763
764
    /**
765
     * Get the match type
766
     *
767
     * @return string 'official', 'fm', or 'special'
768
     */
769
    public function getMatchType()
770
    {
771
        return $this->match_type;
772
    }
773
774
    /**
775
     * Set the match type
776
     *
777
     * @param  string $matchType A valid match type; official, fm, special
778
     *
779
     * @return static
780
     */
781
    public function setMatchType($matchType)
782
    {
783
        return $this->updateProperty($this->match_type, "match_type", $matchType, 's');
784
    }
785
786
    /**
787
     * Get a JSON decoded array of events that occurred during the match
788
     * @return mixed|null Returns null if there were no events recorded for the match
789
     */
790
    public function getMatchDetails()
791
    {
792
        return json_decode($this->match_details);
793
    }
794
795
    /**
796
     * Get the server this match took place on
797
     *
798
     * @return Server
799
     */
800
    public function getServer()
801
    {
802
        return Server::get($this->server);
803
    }
804
805
    /**
806
     * Set the server this match took place on
807
     *
808
     * @param  int $serverID
809
     *
810
     * @return $this
811
     */
812
    public function setServer($serverID = null)
813
    {
814
        $this->updateProperty($this->server, 'server_id', $serverID);
815
816
        return $this;
817
    }
818
819
    /**
820
     * Get the server address of the server where this match took place
821
     *
822
     * @deprecated 0.10.0 Use Match::getServer() instead. Using this function is reserved for migrations/legacy support.
823
     *
824
     * @see 20170912201127_match_server_relationship.php
825
     *
826
     * @return string|null Returns null if there was no server address recorded
827
     */
828
    public function getServerAddress()
829
    {
830
        return $this->server_address;
831
    }
832
833
    /**
834
     * Get the name of the replay file for this specific map
835
     * @param  int    $length The length of the replay file name; it will be truncated
836
     * @return string Returns null if there was no replay file name recorded
837
     */
838
    public function getReplayFileName($length = 0)
839
    {
840
        if ($length > 0) {
841
            return substr($this->replay_file, 0, $length);
842
        }
843
844
        return $this->replay_file;
845
    }
846
847
    /**
848
     * Get the match duration
849
     * @return int The duration of the match in minutes
850
     */
851
    public function getDuration()
852
    {
853
        return $this->duration;
854
    }
855
856
    /**
857
     * Set the match duration
858
     *
859
     * @param  int  $duration The new duration of the match in minutes
860
     * @return self
861
     */
862
    public function setDuration($duration)
863
    {
864
        return $this->updateProperty($this->duration, "duration", $duration);
865
    }
866
867
    /**
868
     * Get the user who entered the match
869
     * @return Player
870
     */
871
    public function getEnteredBy()
872
    {
873
        return Player::get($this->entered_by);
874
    }
875
876
    /**
877
     * Get the loser of the match
878
     *
879
     * @return TeamInterface The team that was the loser or the team with the lower elo if the match was a draw
880
     */
881
    public function getLoser()
882
    {
883
        // Get the winner of the match
884
        $winner = $this->getWinner();
885
886
        // Get the team that wasn't the winner... Duh
887
        return $this->getOpponent($winner);
888
    }
889
890
    /**
891
     * Get the winner of a match
892
     *
893
     * @return TeamInterface The team that was the victor or the team with the lower elo if the match was a draw
894
     */
895
    public function getWinner()
896
    {
897
        // "As Mother Teresa once said, it's not enough if you win. Others must lose."
898
        //   -Stephen Colbert
899
900
        // Get the team that had its Elo increased or the team whose players had their Elo increased
901
        if ($this->elo_diff > 0 || $this->player_elo_diff > 0) {
902
            return $this->getTeamA();
903
        } elseif ($this->elo_diff < 0 || $this->player_elo_diff < 0) {
904
            return $this->getTeamB();
905
        } elseif ($this->team_a_points > $this->team_b_points) {
906
            // In case we're dealing with a match such an FM that doesn't have an ELO difference
907
            return $this->getTeamA();
908
        } elseif ($this->team_a_points < $this->team_b_points) {
909
            return $this->getTeamB();
910
        }
911
912
        // If the scores are the same, return Team A because well, fuck you that's why
913
        return $this->getTeamA();
914
    }
915
916
    /**
917
     * Determine whether the match was a draw
918
     * @return bool True if the match ended without any winning teams
919
     */
920
    public function isDraw()
921
    {
922
        return $this->team_a_points == $this->team_b_points;
923
    }
924
925
    /**
926
     * Find out whether the match involves a team
927
     *
928
     * @param  TeamInterface $team The team to check
929
     * @return bool
930
     */
931
    public function involvesTeam($team)
932
    {
933
        return $team->getId() == $this->getTeamA()->getId() || $team->getId() == $this->getTeamB()->getId();
934
    }
935
936
    /**
937
     * Find out if the match is played between official teams
938
     */
939
    public function isOfficial()
940
    {
941
        return self::OFFICIAL === $this->getMatchType();
942
    }
943
944
    /**
945
     * Reset the ELOs of the teams participating in the match
946
     *
947
     * @return self
948
     */
949
    public function resetTeamElos()
950
    {
951
        if ($this->match_type === self::OFFICIAL) {
952
            $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...
953
            $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...
954
        }
955
956
        return $this;
957
    }
958
959
    /**
960
     * Calculate the match's contribution to the team activity
961
     *
962
     * @return float
963
     */
964
    public function getActivity()
965
    {
966
        $daysPassed = $this->getTimestamp()->diffInSeconds();
967
        $daysPassed = $daysPassed / TimeDate::SECONDS_PER_MINUTE / TimeDate::MINUTES_PER_HOUR / TimeDate::HOURS_PER_DAY;
968
969
        $activity = 0.0116687059537612 * (pow(45 - $daysPassed, (1 / 6)) + atan(31.0 - $daysPassed) / 2.0);
970
971
        if (is_nan($activity) || $activity < 0.0) {
972
            return 0.0;
973
        }
974
975
        return $activity;
976
    }
977
978
    /**
979
     * Calculate the Elo differences for players and teams for a given match.
980
     *
981
     * @param  Team $a
982
     * @param  Team $b
983
     * @param  int  $a_points
984
     * @param  int  $b_points
985
     * @param  int[]|Player[] $a_players
986
     * @param  int[]|Player[] $b_players
987
     * @param  int  $duration
988
     *
989
     * @throws InvalidArgumentException When a "Mixed" team is entered without a player roster
990
     *
991
     * @return array
992
     */
993
    private static function calculateElos($a, $b, $a_points, $b_points, $a_players, $b_players, $duration)
994
    {
995
        // Get the type of official match
996
        $matchType = Match::MIXED_V_MIXED;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
997
998
        if ($a->supportsMatchCount() && $b->supportsMatchCount()) {
999
            $matchType = Match::TEAM_V_TEAM;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1000
        } elseif ($a->supportsMatchCount() xor $b->supportsMatchCount()) {
1001
            $matchType = Match::TEAM_V_MIXED;
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1002
        }
1003
1004
        if ($matchType == Match::TEAM_V_MIXED &&
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1005
            ((!$a->isValid() && empty($a_players)) || (!$b->isValid() && empty($b_players)))) {
1006
            throw new InvalidArgumentException('A Mixed team must have a player roster to calculate the Elo for team Elo differences');
1007
        }
1008
1009
        //
1010
        // Handle Player Elo Diff Calculations
1011
        //
1012
1013
        // By default, we won't have a player Elo difference since we won't force matches to have a roster
1014
        $playerEloDiff = null;
1015
1016
        $a_players_elo = 1200;
1017
        $b_players_elo = 1200;
1018
1019
        // Only bother to calculate a player Elo diff if we have players reported for both teams
1020
        if (!empty($a_players) && !empty($b_players)) {
1021
            $a_players_elo = self::getAveragePlayerElo($a_players);
1022
            $b_players_elo = self::getAveragePlayerElo($b_players);
1023
1024
            $playerEloDiff = self::calculateEloDiff($a_players_elo, $b_players_elo, $a_points, $b_points, $duration);
1025
        }
1026
1027
        //
1028
        // Handle Team Elo Diff Calculations
1029
        //
1030
1031
        // By default, we'll assume a Mixed vs Mixed official match where Elos do not change for teams
1032
        $teamEloDiff = null;
1033
1034
        // Work with calculations for team Elos to handle the following situations:
1035
        //   - Team vs Team  :: Use team Elos for calculations
1036
        //   - Team vs Mixed :: Use team Elo and the player average Elo for the "Mixed" team
1037
        if ($matchType == Match::TEAM_V_TEAM) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1038
            $teamEloDiff = self::calculateEloDiff($a->getElo(), $b->getElo(), $a_points, $b_points, $duration);
1039
        } elseif ($matchType == Match::TEAM_V_MIXED) {
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1040
            $a_team_elo = ($a->supportsMatchCount()) ? $a->getElo() : $a_players_elo;
1041
            $b_team_elo = ($b->supportsMatchCount()) ? $b->getElo() : $b_players_elo;
1042
1043
            $teamEloDiff = self::calculateEloDiff($a_team_elo, $b_team_elo, $a_points, $b_points, $duration);
1044
        }
1045
1046
        return [
1047
            'match_type' => $matchType,
1048
            'team_elo'   => $teamEloDiff,
1049
            'player_elo' => $playerEloDiff
1050
        ];
1051
    }
1052
1053
    /**
1054
     * Enter a new match to the database
1055
     * @param  int             $a          Team A's ID
1056
     * @param  int             $b          Team B's ID
1057
     * @param  int             $a_points   Team A's match points
1058
     * @param  int             $b_points   Team B's match points
1059
     * @param  int             $duration   The match duration in minutes
1060
     * @param  int|null        $entered_by The ID of the player reporting the match
1061
     * @param  string|DateTime $timestamp  When the match was played
1062
     * @param  int[]           $a_players  The IDs of the first team's players
1063
     * @param  int[]           $b_players  The IDs of the second team's players
1064
     * @param  string|null     $server     The address of the server where the match was played
1065
     * @param  string          $replayFile The name of the replay file of the match
1066
     * @param  int             $map        The ID of the map where the match was played, only for rotational leagues
1067
     * @param  string          $matchType  The type of match (e.g. official, fm, special)
1068
     * @param  string          $a_color    Team A's color
1069
     * @param  string          $b_color    Team b's color
1070
     * @param  string[]        $a_ipAddresses The IP addresses of players on Team A. The order of this array should
1071
     *                                        match the order of $a_players
1072
     * @param  string[]        $b_ipAddresses The IP addresses of players on Team B. The order of this array should
1073
     *                                        match the order of $b_players
1074
     * @param  string[]        $a_callsigns   The callsigns of players on Team A. The order of this array should match
1075
     *                                        the order of $a_players
1076
     * @param  string[]        $b_callsigns   The callsigns of players on Team B. The order of this array should match
1077
     *                                        the order of $b_players
1078
     *
1079
     * @throws \Exception               When no testing environment has been configured for the database.
1080
     * @throws InvalidArgumentException When a ColorTeam is selected for an official match and no players are defined
1081
     *                                  for that team
1082
     *
1083
     * @return Match           An object representing the match that was just entered
1084
     */
1085
    public static function enterMatch(
1086
        $a, $b, $a_points, $b_points, $duration, $entered_by, $timestamp = "now",
1087
        $a_players = array(), $b_players = array(), $server = null, $replayFile = null,
1088
        $map = null, $matchType = "official", $a_color = null, $b_color = null,
1089
        $a_ipAddresses = array(), $b_ipAddresses = array(), $a_callsigns = array(), $b_callsigns = array()
1090
    ) {
1091
        $matchData = array(
1092
            'team_a_color'   => strtolower($a_color),
1093
            'team_b_color'   => strtolower($b_color),
1094
            'team_a_points'  => $a_points,
1095
            'team_b_points'  => $b_points,
1096
            'timestamp'      => TimeDate::from($timestamp)->toMysql(),
1097
            'duration'       => $duration,
1098
            'entered_by'     => $entered_by,
1099
            'server'         => $server,
1100
            'replay_file'    => $replayFile,
1101
            'map'            => $map,
1102
            'status'         => 'entered',
1103
            'match_type'     => $matchType
1104
        );
1105
1106
        // (P)layer Elo Diff and (T)eam Elo Diff; respectively
1107
        $tEloDiff = null;
0 ignored issues
show
Unused Code introduced by
$tEloDiff is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1108
1109
        if ($matchType === self::OFFICIAL) {
1110
            $team_a = Team::get($a);
1111
            $team_b = Team::get($b);
1112
1113
            $eloCalcs = self::calculateElos($team_a, $team_b, $a_points, $b_points, $a_players, $b_players, $duration);
1114
1115
            $matchData['elo_diff'] = $tEloDiff = $eloCalcs['team_elo'];
1116
            $matchData['player_elo_diff'] = $eloCalcs['player_elo'];
1117
1118
            // Update team ELOs
1119
            if ($team_a->isValid()) {
1120
                $team_a->adjustElo($tEloDiff);
1121
1122
                $matchData['team_a'] = $a;
1123
                $matchData['team_a_elo_new'] = $team_a->getElo();
1124
            }
1125
            if ($team_b->isValid()) {
1126
                $team_b->adjustElo(-$tEloDiff);
1127
1128
                $matchData['team_b'] = $b;
1129
                $matchData['team_b_elo_new'] = $team_b->getElo();
1130
            }
1131
        }
1132
1133
        $match = self::create($matchData, 'updated');
1134
        $match->updateMatchCount();
1135
        $match->updatePlayerElo();
1136
1137
        $matchParticipation = [];
1138
        $dataBuilder = function ($playerIDs, $callsigns, $ipAddresses, $isTeamA) use (&$matchParticipation, $match, $matchData) {
1139
            foreach ($playerIDs as $index => $playerID) {
1140
                if (empty($playerID)) {
1141
                    continue;
1142
                }
1143
1144
                $workspace = [
1145
                    'match_id' => $match->getId(),
1146
                    'user_id' => $playerID,
1147
                    'callsign' => $callsigns[$index],
1148
                    'ip_address' => $ipAddresses[$index],
1149
                    'team_loyalty' => (int)$isTeamA,
1150
                ];
1151
1152 View Code Duplication
                if ($matchData['team_a'] !== null && $isTeamA) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1153
                    $workspace['team_id'] = $matchData['team_a'];
1154
                } elseif ($matchData['team_b'] !== null && !$isTeamA) {
1155
                    $workspace['team_id'] = $matchData['team_b'];
1156
                }
1157
1158
                $matchParticipation[] = $workspace;
1159
            }
1160
        };
1161
        $dataBuilder($a_players, $a_callsigns, $a_ipAddresses, true);
1162
        $dataBuilder($b_players, $b_callsigns, $b_ipAddresses, false);
1163
1164
        $db = Database::getInstance();
1165
        $db->insertBatch('match_participation', $matchParticipation);
1166
1167
        return $match;
1168
    }
1169
1170
    /**
1171
     * Calculate the ELO score difference
1172
     *
1173
     * Computes the ELO score difference on each team after a match, based on
1174
     * GU League's rules.
1175
     *
1176
     * @param  int $a_elo    Team A's current ELO score
1177
     * @param  int $b_elo    Team B's current ELO score
1178
     * @param  int $a_points Team A's match points
1179
     * @param  int $b_points Team B's match points
1180
     * @param  int $duration The match duration in minutes
1181
     * @return int The ELO score difference
1182
     */
1183
    public static function calculateEloDiff($a_elo, $b_elo, $a_points, $b_points, $duration)
1184
    {
1185
        $prob = 1.0 / (1 + pow(10, (($b_elo - $a_elo) / 400.0)));
1186
        if ($a_points > $b_points) {
1187
            $diff = 50 * (1 - $prob);
1188
        } elseif ($a_points == $b_points) {
1189
            $diff = 50 * (0.5 - $prob);
1190
        } else {
1191
            $diff = 50 * (0 - $prob);
1192
        }
1193
1194
        // Apply ELO modifiers from `config.yml`
1195
        $durations = Service::getParameter('bzion.league.duration');
1196
        $diff *= (isset($durations[$duration])) ? $durations[$duration] : 1;
1197
1198
        if (abs($diff) < 1 && $diff != 0) {
1199
            // ELOs such as 0.75 should round up to 1...
1200
            return ($diff > 0) ? 1 : -1;
1201
        }
1202
1203
        // ...everything else is rounded down (-3.7 becomes -3 and 48.1 becomes 48)
1204
        return intval($diff);
1205
    }
1206
1207
    /**
1208
     * Find if a match's stored ELO is correct
1209
     */
1210
    public function isEloCorrect()
1211
    {
1212
        return $this->elo_diff === $this->calculateEloDiff(
1213
            $this->getTeamAEloOld(),
1214
            $this->getTeamBEloOld(),
1215
            $this->getTeamAPoints(),
1216
            $this->getTeamBPoints(),
1217
            $this->getDuration()
1218
        );
1219
    }
1220
1221
    /**
1222
     * Remove Elo recordings for players participating in this match
1223
     */
1224
    public function resetPlayerElos()
1225
    {
1226
        $this->db->execute('DELETE FROM player_elo WHERE match_id = ?', [$this->getId()]);
1227
    }
1228
1229
    /**
1230
     * Recalculate the match's elo and adjust the team ELO values
1231
     */
1232
    public function recalculateElo()
1233
    {
1234
        if ($this->match_type !== self::OFFICIAL) {
1235
            return;
1236
        }
1237
1238
        $a = $this->getTeamA();
1239
        $b = $this->getTeamB();
1240
1241
        $this->resetPlayerElos();
1242
1243
        foreach ($this->getPlayers() as $player) {
0 ignored issues
show
Bug introduced by
The expression $this->getPlayers() 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...
1244
            $player->invalidateMatchFromCache($this);
1245
        }
1246
1247
        $eloCalcs = self::calculateElos(
1248
            $a, $b,
0 ignored issues
show
Compatibility introduced by
$a of type object<TeamInterface> is not a sub-type of object<Team>. It seems like you assume a concrete implementation of the interface TeamInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
Compatibility introduced by
$b of type object<TeamInterface> is not a sub-type of object<Team>. It seems like you assume a concrete implementation of the interface TeamInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
1249
            $this->getTeamAPoints(), $this->getTeamBPoints(),
1250
            $this->getTeamAPlayers(), $this->getTeamBPlayers(),
1251
            $this->getDuration()
1252
        );
1253
1254
        $elo = $eloCalcs['team_elo'];
1255
1256
        $this->updateProperty($this->elo_diff, 'elo_diff', $elo);
1257
        $this->updateProperty($this->player_elo_diff, 'player_elo_diff', $eloCalcs['player_elo']);
1258
1259
        if ($a->supportsMatchCount()) {
1260
            $a->adjustElo($elo);
1261
            $this->updateProperty($this->team_a_elo_new, 'team_a_elo_new', $a->getElo());
1262
        }
1263
1264
        if ($b->supportsMatchCount()) {
1265
            $b->adjustElo(-$elo);
1266
            $this->updateProperty($this->team_b_elo_new, 'team_b_elo_new', $b->getElo());
1267
        }
1268
1269
        $this->updatePlayerElo();
1270
    }
1271
1272
    /**
1273
     * Get all the matches in the database
1274
     */
1275
    public static function getMatches()
1276
    {
1277
        return self::getQueryBuilder()->active()->getModels();
1278
    }
1279
1280
    /**
1281
     * Get a query builder for matches
1282
     * @return MatchQueryBuilder
1283
     */
1284
    public static function getQueryBuilder()
1285
    {
1286
        return new MatchQueryBuilder('Match', array(
1287
            'columns' => array(
1288
                'firstTeam'        => 'team_a',
1289
                'secondTeam'       => 'team_b',
1290
                'firstTeamPoints'  => 'team_a_points',
1291
                'secondTeamPoints' => 'team_b_points',
1292
                'time'             => 'timestamp',
1293
                'map'              => 'map',
1294
                'server'           => 'server_id',
1295
                'type'             => 'match_type',
1296
                'status'           => 'status'
1297
            ),
1298
        ));
1299
    }
1300
1301
    /**
1302
     * {@inheritdoc}
1303
     */
1304
    public function delete()
1305
    {
1306
        $this->updateMatchCount(true);
1307
1308
        parent::delete();
1309
    }
1310
1311
    /**
1312
     * {@inheritdoc}
1313
     */
1314
    public function getName()
1315
    {
1316
        $description = '';
1317
1318
        switch ($this->getMatchType()) {
1319
            case self::OFFICIAL:
1320
                // Only show Elo diff if both teams are actual teams
1321
                if ($this->getTeamA()->supportsMatchCount() && $this->getTeamB()->supportsMatchCount()) {
1322
                    $description = "(+/- {$this->getEloDiff()})";
1323
                }
1324
                break;
1325
1326
            case self::FUN:
1327
                $description = 'Fun Match:';
1328
                break;
1329
1330
            case self::SPECIAL:
1331
                $description = 'Special Match:';
1332
                break;
1333
1334
            default:
1335
                break;
1336
        }
1337
1338
        return trim(sprintf('%s %s [%d] vs [%d] %s',
1339
            $description,
1340
            $this->getWinner()->getName(),
1341
            $this->getScore($this->getWinner()),
1342
            $this->getScore($this->getLoser()),
1343
            $this->getLoser()->getName()
1344
        ));
1345
    }
1346
1347
    /**
1348
     * Recalculates match history for all teams and matches
1349
     *
1350
     * Recalculation is done as follows:
1351
     * 1. A match is chosen as a starting point - it's stored old team ELOs are
1352
     *    considered correct
1353
     * 2. Team ELOs are reset to their values at the starting point
1354
     * 3. Each match that occurred since the first specified match has its ELO
1355
     *    recalculated based on the current team values, and the new match data
1356
     *    and team ELOs are stored in the database
1357
     *
1358
     * @param Match $match The first match
1359
     *
1360
     * @throws Exception
1361
     */
1362
    public static function recalculateMatchesSince(Match $match)
1363
    {
1364
        try {
1365
            // Commented out to prevent ridiculously large recalculations
1366
            //set_time_limit(0);
1367
1368
            $query = Match::getQueryBuilder()
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1369
                ->where('status')->notEquals('deleted')
1370
                ->where('type')->equals(Match::OFFICIAL)
0 ignored issues
show
Coding Style introduced by
As per coding style, self should be used for accessing local static members.

This check looks for accesses to local static members using the fully qualified name instead of self::.

<?php

class Certificate {
    const TRIPLEDES_CBC = 'ASDFGHJKL';

    private $key;

    public function __construct()
    {
        $this->key = Certificate::TRIPLEDES_CBC;
    }
}

While this is perfectly valid, the fully qualified name of Certificate::TRIPLEDES_CBC could just as well be replaced by self::TRIPLEDES_CBC. Referencing local members with self:: assured the access will still work when the class is renamed, makes it perfectly clear that the member is in fact local and will usually be shorter.

Loading history...
1371
                ->where('time')->isAfter($match->getTimestamp(), $inclusive = true)
1372
                ->sortBy('time');
1373
1374
            /** @var Match[] $matches */
1375
            $matches = $query->getModels($fast = true);
1376
1377
            // Send the total count to client-side javascript
1378
            echo count($matches) . "\n";
1379
1380
            // Start a transaction so tables are locked and we don't stay with
1381
            // messed up data if something goes wrong
1382
            Database::getInstance()->startTransaction();
1383
1384
            $teamsReset = [];
1385
1386
            // Reset match teams, in case the selected match is deleted and does
1387
            // not show up in the list of matches to recalculate
1388
            if ($match->getTeamA()->supportsMatchCount()) {
1389
                $match->getTeamA()->setElo($match->getTeamAEloOld());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method setElo() 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...
1390
                $teamsReset[ $match->getTeamA()->getId() ] = true;
1391
            }
1392
            if ($match->getTeamB()->supportsMatchCount()) {
1393
                $match->getTeamB()->setElo($match->getTeamBEloOld());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method setElo() 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...
1394
                $teamsReset[ $match->getTeamB()->getId() ] = true;
1395
            }
1396
1397
            foreach ($matches as $i => &$match) {
1398
                // Reset teams' ELOs if they haven't been reset already
1399 View Code Duplication
                if ($match->getTeamA()->supportsMatchCount() && !isset($teamsReset[ $match->getTeamA()->getId() ])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1400
                    $teamsReset[ $match->getTeamA()->getId() ] = true;
1401
                    $match->getTeamA()->setElo($match->getTeamAEloOld());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method setElo() 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...
1402
                }
1403 View Code Duplication
                if ($match->getTeamB()->supportsMatchCount() && !isset($teamsReset[ $match->getTeamB()->getId() ])) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
1404
                    $teamsReset[ $match->getTeamB()->getId() ] = true;
1405
                    $match->getTeamB()->setElo($match->getTeamBEloOld());
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface TeamInterface as the method setElo() 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...
1406
                }
1407
1408
                $match->recalculateElo();
1409
1410
                // Send an update to the client-side javascript, so that a
1411
                // progress bar can be updated
1412
                echo "m";
1413
            }
1414
        } catch (Exception $e) {
1415
            Database::getInstance()->rollback();
1416
            Database::getInstance()->finishTransaction();
1417
            throw $e;
1418
        }
1419
1420
        Database::getInstance()->finishTransaction();
1421
1422
        echo "\n\nCalculation successful\n";
1423
    }
1424
1425
    /**
1426
     * Get the average ELO for an array of players
1427
     *
1428
     * @param int[]|Player[] $players
1429
     *
1430
     * @return float|int
1431
     */
1432
    private static function getAveragePlayerElo($players)
1433
    {
1434
        $getElo = function ($n) {
1435
            if ($n instanceof Player) {
1436
                return $n->getElo();
1437
            }
1438
1439
            return Player::get($n)->getElo();
1440
        };
1441
1442
        return array_sum(array_map($getElo, $players)) / count($players);
1443
    }
1444
1445
    /**
1446
     * Update the match count of the teams participating in the match
1447
     *
1448
     * @param bool $decrement Whether to decrement instead of incrementing the match count
1449
     */
1450
    private function updateMatchCount($decrement = false)
1451
    {
1452
        if ($this->match_type !== self::OFFICIAL) {
1453
            return;
1454
        }
1455
1456
        $diff = ($decrement) ? -1 : 1;
1457
1458
        if ($this->isDraw()) {
1459
            $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...
1460
            $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...
1461
        } else {
1462
            $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...
1463
            $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...
1464
        }
1465
    }
1466
1467
    /**
1468
     * Update the Elos for the participating players in a match
1469
     */
1470
    private function updatePlayerElo()
1471
    {
1472
        if ($this->match_type !== self::OFFICIAL || $this->getPlayerEloDiff() === null) {
1473
            return;
1474
        }
1475
1476
        $eloDiff = $this->getPlayerEloDiff(false);
1477
1478
        foreach ($this->getTeamAPlayers() as $player) {
1479
            $player->adjustElo($eloDiff, $this);
1480
            $player->setLastMatch($this->getId());
1481
        }
1482
1483
        foreach ($this->getTeamBPlayers() as $player) {
1484
            $player->adjustElo(-$eloDiff, $this);
1485
            $player->setLastMatch($this->getId());
1486
        }
1487
    }
1488
}
1489