Passed
Push — master ( 24a4a4...d8896b )
by Julien
06:15
created

SingleEliminationTreeGen::pushEmptyGroupsToTree()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.7666
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
namespace Xoco70\LaravelTournaments\TreeGen;
4
5
use Illuminate\Support\Collection;
6
use Xoco70\LaravelTournaments\Exceptions\TreeGenerationException;
7
use Xoco70\LaravelTournaments\Models\ChampionshipSettings;
8
use Xoco70\LaravelTournaments\Models\PreliminaryFight;
9
use Xoco70\LaravelTournaments\Models\SingleEliminationFight;
10
11
abstract class SingleEliminationTreeGen extends TreeGen
12
{
13
    /**
14
     * Calculate the Byes needed to fill the Championship Tree.
15
     *
16
     * @param $fighters
17
     *
18
     * @return Collection
19
     */
20 View Code Duplication
    protected function getByeGroup($fighters)
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...
21
    {
22
        $fighterCount = $fighters->count();
23
        $firstRoundGroupSize = $this->firstRoundGroupSize(); // Get the size of groups in the first round (2,3,4)
24
        // Max number of fighters for the first round
25
        $treeSize = $this->getTreeSize($fighterCount, $firstRoundGroupSize);
26
        $byeCount = $treeSize - $fighterCount;
27
        return $this->createByeGroup($byeCount);
28
    }
29
30
    /**
31
     * Save Groups with their parent info.
32
     *
33
     * @param int $numRounds
34
     * @param int $numFighters
35
     */
36
    protected function pushGroups($numRounds, $numFighters)
37
    {
38
        // TODO Here is where you should change when enable several winners for preliminary
39 View Code Duplication
        for ($roundNumber = 2; $roundNumber <= $numRounds + 1; $roundNumber++) {
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...
40
            // From last match to first match
41
            $maxMatches = ($numFighters / pow(2, $roundNumber));
42
43
            for ($matchNumber = 1; $matchNumber <= $maxMatches; $matchNumber++) {
44
                $fighters = $this->createByeGroup(2);
45
                $group = $this->saveGroup($matchNumber, $roundNumber, null);
46
                $this->syncGroup($group, $fighters);
47
            }
48
        }
49
        // Third place Group
50
        if ($numFighters >= $this->championship->getGroupSize() * 2) {
51
            $fighters = $this->createByeGroup(2);
52
53
            $group = $this->saveGroup($maxMatches, $numRounds, null);
0 ignored issues
show
Bug introduced by
The variable $maxMatches does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
54
55
            $this->syncGroup($group, $fighters);
56
        }
57
    }
58
59
    /**
60
     * Create empty groups after round 1.
61
     *
62
     * @param $numFighters
63
     */
64
    protected function pushEmptyGroupsToTree($numFighters)
65
    {
66
        if ($this->championship->hasPreliminary()) {
67
            /* Should add * prelimWinner but it add complexity about parent / children in tree
68
            */
69
            $numFightersElim = $numFighters / $this->settings->preliminaryGroupSize * 2;
70
            // We calculate how much rounds we will have
71
            $numRounds = intval(log($numFightersElim, 2)); // 3 rounds, but begining from round 2 ( ie => 4)
72
            return $this->pushGroups($numRounds, $numFightersElim);
73
        }
74
        // We calculate how much rounds we will have
75
        $numRounds = $this->getNumRounds($numFighters);
76
77
        return $this->pushGroups($numRounds, $numFighters);
78
    }
79
80
    /**
81
     * Chunk Fighters into groups for fighting, and optionnaly shuffle.
82
     *
83
     * @param $fightersByEntity
84
     *
85
     * @return Collection|null
86
     */
87
    protected function chunk(Collection $fightersByEntity)
88
    {
89
        //TODO Should Pull down to know if team or competitor
90
        if ($this->championship->hasPreliminary()) {
91
            return (new PlayOffCompetitorTreeGen($this->championship, null))->chunk($fightersByEntity);
0 ignored issues
show
Bug introduced by
The method chunk() cannot be called from this context as it is declared protected in class Xoco70\LaravelTournaments\TreeGen\PlayOffTreeGen.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
92
        }
93
        $fightersGroup = $fightersByEntity->chunk(2);
94
        return $fightersGroup;
95
    }
96
97
    /**
98
     * Generate First Round Fights.
99
     */
100
    protected function generateFights()
101
    {
102
        //  First Round Fights
103
        $settings = $this->settings;
104
        $initialRound = 1;
105
        // Very specific case to common case : Preliminary with 3 fighters
106
        if ($this->championship->hasPreliminary() && $settings->preliminaryGroupSize == 3) {
107
            // First we make all first fights of all groups
108
            // Then we make all second fights of all groups
109
            // Then we make all third fights of all groups
110
            $groups = $this->championship->groupsByRound(1)->get();
111
            foreach ($groups as $numGroup => $group) {
112
                for ($numFight = 1; $numFight <= $settings->preliminaryGroupSize; $numFight++) {
113
                    $fight = new PreliminaryFight();
114
                    $order = $numGroup * $settings->preliminaryGroupSize + $numFight;
115
                    $fight->saveFight2($group, $numFight, $order);
116
                }
117
            }
118
            $initialRound++;
119
        }
120
        // Save Next rounds
121
        $fight = new SingleEliminationFight();
122
        $fight->saveFights($this->championship, $initialRound);
123
    }
124
125
    /**
126
     * Return number of rounds for the tree based on fighter count.
127
     *
128
     * @param $numFighters
129
     *
130
     * @return int
131
     */
132
    protected function getNumRounds($numFighters)
133
    {
134
        return intval(log($numFighters / $this->firstRoundGroupSize() * 2, 2));
135
    }
136
137
    /**
138
     * Get the group size for the first row
139
     *
140
     * @return int - return 2 if not preliminary, 3,4,5 otherwise
141
     */
142
    private function firstRoundGroupSize()
143
    {
144
        return $this->championship->hasPreliminary()
145
            ? $this->settings->preliminaryGroupSize
146
            : 2;
147
    }
148
149
    /**
150
     * Generate all the groups, and assign figthers to group
151
     * @throws TreeGenerationException
152
     */
153
    protected function generateAllTrees()
154
    {
155
        $this->minFightersCheck(); // Check there is enough fighters to generate trees
156
        $usersByArea = $this->getFightersByArea(); // Get fighters by area (reparted by entities and filled with byes)
157
        $this->generateGroupsForRound($usersByArea, 1); // Generate all groups for round 1
158
        $numFighters = count($usersByArea->collapse());
159
        $this->pushEmptyGroupsToTree($numFighters); // Fill tree with empty groups
160
        $this->addParentToChildren($numFighters); // Build the entire tree and fill the next rounds if possible
161
    }
162
163
    /**
164
     * @param Collection $usersByArea
165
     * @param $round
166
     */
167
    public function generateGroupsForRound(Collection $usersByArea, $round)
168
    {
169
        $order = 1;
170
        foreach ($usersByArea as $fightersByEntity) {
171
            // Chunking to make small round robin groups
172
            $chunkedFighters = $this->chunk($fightersByEntity);
173
//            dd($chunkedFighters->toArray());
0 ignored issues
show
Unused Code Comprehensibility introduced by
70% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
174
            foreach ($chunkedFighters as $fighters) {
0 ignored issues
show
Bug introduced by
The expression $chunkedFighters of type object<Illuminate\Support\Collection>|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...
175
                $fighters = $fighters->pluck('id');
176
                $group = $this->saveGroup($order, $round, null);
177
                $this->syncGroup($group, $fighters);
178
                $order++;
179
            }
180
        }
181
    }
182
183
    /**
184
     * Check if there is enough fighters, throw exception otherwise.
185
     *
186
     * @throws TreeGenerationException
187
     */
188
    private function minFightersCheck()
189
    {
190
        $fighters = $this->getFighters();
0 ignored issues
show
Bug introduced by
The method getFighters() does not exist on Xoco70\LaravelTournament...ingleEliminationTreeGen. Did you maybe mean getFightersByEntity()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
191
        $areas = $this->settings->fightingAreas;
192
        $fighterType = $this->championship->category->isTeam
0 ignored issues
show
Documentation introduced by
The property category does not exist on object<Xoco70\LaravelTou...ts\Models\Championship>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
193
            ? trans_choice('laravel-tournaments::core.team', 2)
194
            : trans_choice('laravel-tournaments::core.competitor', 2);
195
196
        $minFighterCount = $fighters->count() / $areas;
197
198
        if ($this->settings->hasPreliminary && $fighters->count() / ($this->settings->preliminaryGroupSize * $areas) < 1) {
199
            throw new TreeGenerationException(trans('laravel-tournaments::core.min_competitor_required', [
200
                'number' => $this->settings->preliminaryGroupSize * $areas,
201
                'fighter_type' => $fighterType,
202
            ]), 422);
203
        }
204
205
        if ($minFighterCount < ChampionshipSettings::MIN_COMPETITORS_BY_AREA) {
206
            throw new TreeGenerationException(trans('laravel-tournaments::core.min_competitor_required', [
207
                'number' => ChampionshipSettings::MIN_COMPETITORS_BY_AREA,
208
                'fighter_type' => $fighterType,
209
            ]), 422);
210
        }
211
    }
212
213
    /**
214
     * Insert byes group alternated with full groups.
215
     *
216
     * @param Collection $fighters - List of fighters
217
     * @param integer $numByeTotal - Quantity of byes to insert
218
     * @return Collection - Full list of fighters including byes
219
     */
220
    private function insertByes(Collection $fighters, $numByeTotal)
221
    {
222
        $bye = $this->createByeFighter();
223
        $groupSize = $this->firstRoundGroupSize();
224
        $frequency = $groupSize != 0
225
            ? (int)floor(count($fighters) / $groupSize / $groupSize)
226
            : -1;
227
        if ($frequency < $groupSize) {
228
            $frequency = $groupSize;
229
        }
230
231
        $newFighters = new Collection();
232
        $count = 0;
233
        $byeCount = 0;
234
        foreach ($fighters as $fighter) {
235
            // Each $frequency(3) try to insert $groupSize byes (3)
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
236
            // Not the first iteration, and at the good frequency, and with $numByeTotal as limit
237
            if ($this->shouldInsertBye($frequency, $count, $byeCount, $numByeTotal)) { //
238
                for ($i = 0; $i < $groupSize; $i++) {
239
                    if ($byeCount < $numByeTotal) {
240
                        $newFighters->push($bye);
241
                        $byeCount++;
242
                    }
243
                }
244
            }
245
            $newFighters->push($fighter);
246
            $count++;
247
        }
248
        return $newFighters;
249
    }
250
251
    /**
252
     * @param $frequency
253
     * @param $groupSize
254
     * @param $count
255
     * @param $byeCount
256
     *
257
     * @return bool
258
     */
259
    private
260
    function shouldInsertBye($frequency, $count, $byeCount, $numByeTotal): bool
261
    {
262
        return $count != 0 && $count % $frequency == 0 && $byeCount < $numByeTotal;
263
    }
264
265
266
    /**
267
     * Method that fills fighters with Bye Groups at the end
268
     * @param $fighters
269
     * @param Collection $fighterGroups
270
     *
271
     * @return Collection
272
     */
273
    public function adjustFightersGroupWithByes($fighters, $fighterGroups): Collection
274
    {
275
        $tmpFighterGroups = clone $fighterGroups;
276
        $numBye = count($this->getByeGroup($fighters));
277
278
        // Get biggest competitor's group
279
        $max = $this->getMaxFightersByEntity($tmpFighterGroups);
280
281
        // We put them so that we can mix them up and they don't fight with another competitor of his entity.
282
        $fighters = $this->repart($fighterGroups, $max);
283
284
        if (!app()->runningUnitTests()) {
285
            $fighters = $fighters->shuffle();
286
        }
287
        // Insert byes to fill the tree.
288
        // Strategy: first, one group full, one group empty with byes, then groups of 2 fighters
289
        $fighters = $this->insertByes($fighters, $numBye);
290
        return $fighters;
291
    }
292
293
    /**
294
     * Get the biggest entity group.
295
     *
296
     * @param $userGroups
297
     *
298
     * @return int
299
     */
300
    private
301
    function getMaxFightersByEntity($userGroups): int
302
    {
303
        return $userGroups
304
            ->sortByDesc(function ($group) {
305
                return $group->count();
306
            })
307
            ->first()
308
            ->count();
309
    }
310
311
    /**
312
     * Repart BYE in the tree,.
313
     *
314
     * @param $fighterGroups
315
     * @param int $max
316
     *
317
     * @return Collection
318
     */
319
    private
320
    function repart($fighterGroups, $max)
321
    {
322
        $fighters = new Collection();
323
        for ($i = 0; $i < $max; $i++) {
324
            foreach ($fighterGroups as $fighterGroup) {
325
                $fighter = $fighterGroup->values()->get($i);
326
                if ($fighter != null) {
327
                    $fighters->push($fighter);
328
                }
329
            }
330
        }
331
        return $fighters;
332
    }
333
}
334