Passed
Push — master ( 14ed1b...ecb8e9 )
by Julien
31:40
created

TreeGen::generateGroupsForRound()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 13
rs 9.4285
cc 3
eloc 8
nc 3
nop 4
1
<?php
2
3
namespace Xoco70\KendoTournaments\TreeGen;
4
5
use Illuminate\Support\Collection;
6
use Xoco70\KendoTournaments\Contracts\TreeGenerable;
7
use Xoco70\KendoTournaments\Exceptions\TreeGenerationException;
8
use Xoco70\KendoTournaments\Models\Championship;
9
use Xoco70\KendoTournaments\Models\ChampionshipSettings;
10
use Xoco70\KendoTournaments\Models\Competitor;
11
use Xoco70\KendoTournaments\Models\FightersGroup;
12
use Xoco70\KendoTournaments\Models\Team;
13
14
class TreeGen implements TreeGenerable
15
{
16
    protected $groupBy;
17
    protected $tree;
18
    public $championship;
19
    public $settings;
20
    protected $numFighters;
21
22
    /**
23
     * @param Championship $championship
24
     * @param $groupBy
25
     */
26
    public function __construct(Championship $championship, $groupBy)
27
    {
28
        $this->championship = $championship;
29
        $this->groupBy = $groupBy;
30
        $this->settings = $championship->settings;
31
        $this->tree = new Collection();
32
    }
33
34
    /**
35
     * Generate tree groups for a championship.
36
     *
37
     * @throws TreeGenerationException
38
     */
39
    public function run()
40
    {
41
        $usersByArea = $this->getFightersByArea();
42
        $numFighters = sizeof($usersByArea->collapse());
43
44
        $this->generateGroupsForRound($usersByArea, $area = 1, $round = 1, $shuffle = 1);
45
        $this->pushEmptyGroupsToTree($numFighters);
46
        $this->addParentToChildren($numFighters);
47
        // Now add parents to all
48
    }
49
50
    /**
51
     * @param $userGroups
52
     *
53
     * @return int
54
     */
55
    private function getMaxFightersByEntity($userGroups): int
56
    {
57
        // Surely there is a Laravel function that does it ;)
58
        $max = 0;
59
        foreach ($userGroups as $userGroup) {
60
            if (count($userGroup) > $max) {
61
                $max = count($userGroup);
62
            }
63
        }
64
        return $max;
65
66
//        return $userGroups
0 ignored issues
show
Unused Code Comprehensibility introduced by
56% 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...
67
//            ->sortByDesc(function ($group) {
68
//                return $group->count();
69
//            })
70
//            ->first()
71
//            ->count();
72
    }
73
74
    /**
75
     * Get Competitor's list ordered by entities
76
     * Countries for Internation Tournament, State for a National Tournament, etc.
77
     *
78
     * @return Collection
79
     */
80
    private function getFightersByEntity($fighters): Collection
81
    {
82
        // Right now, we are treating users and teams as equals.
83
        // It doesn't matter right now, because we only need name attribute which is common to both models
84
85
        // $this->groupBy contains federation_id, association_id, club_id, etc.
86
        if (($this->groupBy) != null) {
87
            $fighterGroups = $fighters->groupBy($this->groupBy); // Collection of Collection
88
        } else {
89
            $fighterGroups = $fighters->chunk(1); // Collection of Collection
90
        }
91
        return $fighterGroups;
92
    }
93
94
    /**
95
     * Calculate the Byes need to fill the Championship Tree.
96
     *
97
     * @param Championship $championship
98
     *
99
     * @return Collection
100
     */
101
    private function getByeGroup(Championship $championship, $fighters)
102
    {
103
        $groupSizeDefault = 3;
104
        $preliminaryGroupSize = 2;
105
106
        $fighterCount = $fighters->count();
107
108
        if ($championship->hasPreliminary()) {
109
            $preliminaryGroupSize = $championship->settings != null
110
                ? $championship->settings->preliminaryGroupSize
111
                : $groupSizeDefault;
112
        } elseif ($championship->isDirectEliminationType()) {
113
            $preliminaryGroupSize = 2;
114
        } else {
0 ignored issues
show
Unused Code introduced by
This else statement is empty and can be removed.

This check looks for the else branches of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These else branches can be removed.

if (rand(1, 6) > 3) {
print "Check failed";
} else {
    //print "Check succeeded";
}

could be turned into

if (rand(1, 6) > 3) {
    print "Check failed";
}

This is much more concise to read.

Loading history...
115
            // No Preliminary and No Direct Elimination --> Round Robin
116
            // Should Have no tree
117
        }
118
        $treeSize = $this->getTreeSize($fighterCount, $preliminaryGroupSize);
119
120
        $byeCount = $treeSize - $fighterCount;
121
122
        return $this->createNullsGroup($byeCount, $championship->category->isTeam);
123
    }
124
125
    /**
126
     * @param $fighterCount
127
     *
128
     * @return int
129
     */
130
    private function getTreeSize($fighterCount, $groupSize)
131
    {
132
        $square = collect([1, 2, 4, 8, 16, 32, 64]);
133
        $squareMultiplied = $square->map(function ($item, $key) use ($groupSize) {
0 ignored issues
show
Unused Code introduced by
The parameter $key 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...
134
            return $item * $groupSize;
135
        });
136
137
        foreach ($squareMultiplied as $limit) {
138
            if ($fighterCount <= $limit) {
139
                return $limit;
140
            }
141
        }
142
        return 64 * $groupSize;
143
    }
144
145
    /**
146
     * @param $byeCount
147
     *
148
     * @return Collection
149
     */
150
    private function createNullsGroup($byeCount, $isTeam): Collection
151
    {
152
        $isTeam
153
            ? $null = new Team()
154
            : $null = new Competitor();
155
156
        $byeGroup = new Collection();
157
        for ($i = 0; $i < $byeCount; $i++) {
158
            $byeGroup->push($null);
159
        }
160
161
        return $byeGroup;
162
    }
163
164
    /**
165
     * @param $fighterGroups
166
     * @param int $max
167
     *
168
     * @return Collection
169
     */
170
    private function repart($fighterGroups, $max)
171
    {
172
        $fighters = new Collection();
173
        for ($i = 0; $i < $max; $i++) {
174
            foreach ($fighterGroups as $fighterGroup) {
175
                $fighter = $fighterGroup->values()->get($i);
176
                if ($fighter != null) {
177
                    $fighters->push($fighter);
178
                }
179
            }
180
        }
181
182
        return $fighters;
183
    }
184
185
    /**
186
     * Insert byes in an homogen way.
187
     *
188
     * @param Collection $fighters
189
     * @param Collection $byeGroup
190
     *
191
     * @return Collection
192
     */
193
    private function insertByes(Collection $fighters, Collection $byeGroup)
194
    {
195
        $bye = count($byeGroup) > 0 ? $byeGroup[0] : [];
196
        $sizeFighters = count($fighters);
197
        $sizeGroupBy = count($byeGroup);
198
199
        $frequency = $sizeGroupBy != 0
200
            ? (int)floor($sizeFighters / $sizeGroupBy)
201
            : -1;
202
203
        // Create Copy of $competitors
204
        $newFighters = new Collection();
205
        $i = 0;
206
        $byeCount = 0;
207
        foreach ($fighters as $fighter) {
208
            if ($frequency != -1 && $i % $frequency == 0 && $byeCount < $sizeGroupBy) {
209
                $newFighters->push($bye);
210
                $byeCount++;
211
            }
212
            $newFighters->push($fighter);
213
            $i++;
214
        }
215
216
        return $newFighters;
217
    }
218
219
    private function getFighters()
220
    {
221
        $this->championship->category->isTeam()
222
            ? $fighters = $this->championship->teams
223
            : $fighters = $this->championship->competitors;
224
225
        return $fighters;
226
    }
227
228
    /**
229
     * @param $usersByArea
230
     * @param $area
231
     *
232
     */
233
    public function generateGroupsForRound($usersByArea, $area, $round, $shuffle)
234
    {
235
        foreach ($usersByArea as $fightersByEntity) {
236
            // Chunking to make small round robin groups
237
            $fightersGroup = $this->chunkAndShuffle($round, $shuffle, $fightersByEntity);
238
            $order = 1;
239
            foreach ($fightersGroup as $value => $fighters) {
240
                $this->saveGroupAndSync($fighters, $area, $order, $round, $parent = null, $shuffle);
241
                $order++;
242
            }
243
            $area++;
244
        }
245
    }
246
247
    /**
248
     * @param $fighters
249
     * @param $area
250
     * @param $order
251
     * @param $round
252
     * @return FightersGroup
253
     */
254
    public function saveGroupAndSync($fighters, $area, $order, $round, $parent, $shuffle)
255
    {
256
257
        $fighters = $fighters->pluck('id');
258
        if ($shuffle) $fighters->shuffle();
259
        $group = $this->saveGroup($area, $order, $round, $parent);
260
261
        // Add all competitors to Pivot Table
262
        if ($this->championship->category->isTeam()) {
263
            $group->syncTeams($fighters);
264
        } else {
265
            $group->syncCompetitors($fighters);
266
        }
267
268
        return $group;
269
    }
270
271
    /**
272
     * Create empty groups for direct Elimination Tree
273
     * @param $numFighters
274
     */
275
    public function pushEmptyGroupsToTree($numFighters)
276
    {
277
        $numFightersEliminatory = $numFighters;
278
        // We check what will be the number of groups after the preliminaries
279
        if ($this->championship->hasPreliminary()) {
280
            $numFightersEliminatory = $numFighters / $this->championship->getSettings()->preliminaryGroupSize * 2;
281
        }
282
        // We calculate how much rounds we will have
283
        $numRounds = intval(log($numFightersEliminatory, 2));
284
        $this->pushGroups($numRounds, $numFightersEliminatory, $shuffle = 1);
0 ignored issues
show
Documentation introduced by
$shuffle = 1 is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
285
    }
286
287
    /**
288
     * @param $area
289
     * @param $order
290
     * @param $round
291
     * @param $parent
292
     * @return FightersGroup
293
     */
294
    private function saveGroup($area, $order, $round, $parent): FightersGroup
295
    {
296
        $group = new FightersGroup();
297
        $group->area = $area;
298
        $group->order = $order;
299
        $group->round = $round;
300
        $group->championship_id = $this->championship->id;
301
        if ($parent != null) {
302
            $group->parent_id = $parent->id;
303
        }
304
        $group->save();
305
        return $group;
306
    }
307
308
    private function createByeFighter()
309
    {
310
        return $this->championship->category->isTeam
311
            ? new Team()
312
            : new Competitor();
313
    }
314
315
    public function createByeGroup($groupSize): Collection
316
    {
317
        $byeFighter = $this->createByeFighter();
318
        $group = new Collection();
319
        for ($i = 0; $i < $groupSize; $i++) {
320
            $group->push($byeFighter);
321
        }
322
        return $group;
323
    }
324
325
    /**
326
     * @param $fighters
327
     * @param $fighterGroups
328
     * @return Collection
329
     */
330
    public function adjustFightersGroupWithByes($fighters, $fighterGroups): Collection
331
    {
332
        $tmpFighterGroups = clone $fighterGroups;
333
334
        $byeGroup = $this->getByeGroup($this->championship, $fighters);
335
336
        // Get biggest competitor's group
337
        $max = $this->getMaxFightersByEntity($tmpFighterGroups);
338
339
        // We reacommodate them so that we can mix them up and they don't fight with another competitor of his entity.
340
341
        $fighters = $this->repart($fighterGroups, $max);
342
        $fighters = $this->insertByes($fighters, $byeGroup);
343
344
        return $fighters;
345
    }
346
347
    /**
348
     * Get All Groups on previous round
349
     * @param $currentRound
350
     * @return Collection
351
     */
352
    private function getPreviousRound($currentRound)
353
    {
354
        $previousRound = $this->championship->groupsByRound($currentRound + 1)->get();
355
        dump("previous". $previousRound->pluck('id'));
356
        return $previousRound;
357
    }
358
359
    /**
360
     * Get the next group on the right ( parent ), final round being the ancestor
361
     * @param $matchNumber
362
     * @param $previousRound
363
     * @return mixed
364
     */
365
    private function getParentGroup($matchNumber, $previousRound)
366
    {
367
        $parentIndex = intval(($matchNumber + 1) / 2);
368
        $parent = $previousRound->get($parentIndex - 1);
369
        return $parent;
370
    }
371
372
    /**
373
     * Save Groups with their parent info
374
     * @param $numRounds
375
     * @param $numFightersEliminatory
376
     */
377
    private function pushGroups($numRounds, $numFightersEliminatory, $shuffle = true)
378
    {
379
        for ($roundNumber = 2; $roundNumber <= $numRounds; $roundNumber++) {
380
            // From last match to first match
381
            for ($matchNumber = 1; $matchNumber <= ($numFightersEliminatory / pow(2, $roundNumber)); $matchNumber++) {
382
                $fighters = $this->createByeGroup(2);
383
                $this->saveGroupAndSync($fighters, $area = 1, $order = $matchNumber, $roundNumber, $parent = null, $shuffle);
384
            }
385
        }
386
    }
387
388
    /**
389
     * @return Collection
390
     * @throws TreeGenerationException
391
     */
392
    private function getFightersByArea()
393
    {
394
        // If previous trees already exists, delete all
395
        $this->championship->fightersGroups()->delete();
396
        $areas = $this->settings->fightingAreas;
397
        $fighters = $this->getFighters();
398
399
        if ($fighters->count() / $areas < ChampionshipSettings::MIN_COMPETITORS_BY_AREA) {
400
            throw new TreeGenerationException();
401
        }
402
        // Get Competitor's / Team list ordered by entities ( Federation, Assoc, Club, etc...)
403
        $fighterByEntity = $this->getFightersByEntity($fighters); // Chunk(1)
404
        $fightersWithBye = $this->adjustFightersGroupWithByes($fighters, $fighterByEntity);
405
406
        // Chunk user by areas
407
        return $fightersWithBye->chunk(count($fightersWithBye) / $areas);
408
    }
409
410
    /**
411
     * @param $round
412
     * @param $shuffle
413
     * @param $fightersByEntity
414
     * @return mixed
415
     */
416
    private function chunkAndShuffle($round, $shuffle, $fightersByEntity)
417
    {
418
        if ($this->championship->hasPreliminary()) {
419
            $fightersGroup = $fightersByEntity->chunk($this->settings->preliminaryGroupSize);
420
            if ($shuffle) $fightersGroup->shuffle();
421
        } elseif ($this->championship->isDirectEliminationType() || $round > 1) {
422
            $fightersGroup = $fightersByEntity->chunk(2);
423
            if ($shuffle) $fightersGroup->shuffle();
424
        } else { // Round Robin
425
            $fightersGroup = $fightersByEntity->chunk($fightersByEntity->count());
426
        }
427
        return $fightersGroup;
428
    }
429
430
    /**
431
     * Attach a parent to every child for nestedSet Navigation
432
     */
433
    private function addParentToChildren($numFightersEliminatory)
434
    {
435
        $numRounds = intval(log($numFightersEliminatory, 2));
436
437
        $groupsDesc = $this->championship
438
            ->fightersGroups()
439
            ->where('round', '<', $numRounds)
440
            ->orderByDesc('id')->get();
441
442
        $groupsDescByRound = $groupsDesc->groupBy('round');
443
444
        foreach ($groupsDescByRound as $round => $groups) {
445
            $previousRound = $this->getPreviousRound($round, $numRounds);
0 ignored issues
show
Unused Code introduced by
The call to TreeGen::getPreviousRound() has too many arguments starting with $numRounds.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
446
            foreach ($groups->reverse()->values() as $matchNumber => $group) {
447
                $parent = $this->getParentGroup($matchNumber + 1, $previousRound);
448
                $group->parent_id = $parent->id;
449
                $group->save();
450
            }
451
        }
452
453
454
    }
455
}
456