Passed
Branch master (8db9fa)
by Julien
24:59
created

TreeGen::adjustFightersGroupWithByes()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 16
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 2
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
21
    /**
22
     * @param \Xoco70\KendoTournaments\Models\ChampionshipSettings $settings
23
     */
24
    public function __construct(Championship $championship, $groupBy, $settings)
25
    {
26
        $this->championship = $championship;
27
        $this->groupBy = $groupBy;
28
        $this->settings = $settings;
29
    }
30
31
    /**
32
     * Generate tree groups for a championship.
33
     *
34
     * @throws TreeGenerationException
35
     *
36
     * @return Collection
37
     */
38
    public function run()
39
    {
40
        $this->tree = new Collection();
41
        // If previous trees already exists, delete all
42
        $this->championship->fightersGroups()->delete();
43
        $areas = $this->settings->fightingAreas;
44
        $fighters = $this->getFighters();
45
46
        if ($fighters->count() / $areas < ChampionshipSettings::MIN_COMPETITORS_BY_AREA) {
47
            throw new TreeGenerationException();
48
        }
49
50
        // Get Competitor's / Team list ordered by entities ( Federation, Assoc, Club, etc...)
51
        $fighterGroups = $this->getFightersByEntity($fighters);
52
53
        $fightersByEntity = $this->adjustFightersGroupWithByes($fighters, $fighterGroups);
54
55
        // Chunk user by areas
56
57
        $usersByArea = $fightersByEntity->chunk(count($fightersByEntity) / $areas);
58
59
        $area = 1;
60
        $round = 1;
61
        $numFighters = sizeof($usersByArea->collapse());
62
63
        $this->pushEmptyGroupsToTree($numFighters);
64
        $this->generateGroupsForRound($usersByArea, $area, $round);
65
        
66
        return $this->tree;
67
    }
68
69
    /**
70
     * @param $userGroups
71
     *
72
     * @return int
73
     */
74
    private function getMaxFightersByEntity($userGroups): int
75
    {
76
        // Surely there is a Laravel function that does it ;)
77
        $max = 0;
78
        foreach ($userGroups as $userGroup) {
79
            if (count($userGroup) > $max) {
80
                $max = count($userGroup);
81
            }
82
        }
83
84
        return $max;
85
    }
86
87
    /**
88
     * Get Competitor's list ordered by entities
89
     * Countries for Internation Tournament, State for a National Tournament, etc.
90
     *
91
     * @return Collection
92
     */
93
    private function getFightersByEntity($fighters): Collection
94
    {
95
        // Right now, we are treating users and teams as equals.
96
        // It doesn't matter right now, because we only need name attribute which is common to both models
97
98
        // $this->groupBy contains federation_id, association_id, club_id, etc.
99
        if (($this->groupBy) != null) {
100
            $fighterGroups = $fighters->groupBy($this->groupBy); // Collection of Collection
101
        } else {
102
            $fighterGroups = $fighters->chunk(1); // Collection of Collection
103
        }
104
        return $fighterGroups;
105
    }
106
107
    /**
108
     * Calculate the Byes need to fill the Championship Tree.
109
     *
110
     * @param Championship $championship
111
     *
112
     * @return Collection
113
     */
114
    private function getByeGroup(Championship $championship, $fighters)
115
    {
116
        $groupSizeDefault = 3;
117
        $preliminaryGroupSize = 2;
118
119
        $fighterCount = $fighters->count();
120
121
        if ($championship->hasPreliminary()) {
122
            $preliminaryGroupSize = $championship->settings != null
123
                ? $championship->settings->preliminaryGroupSize
124
                : $groupSizeDefault;
125
        } elseif ($championship->isDirectEliminationType()) {
126
            $preliminaryGroupSize = 2;
127
        } 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...
128
            // No Preliminary and No Direct Elimination --> Round Robin
129
            // Should Have no tree
130
        }
131
        $treeSize = $this->getTreeSize($fighterCount, $preliminaryGroupSize);
132
133
        $byeCount = $treeSize - $fighterCount;
134
135
        return $this->createNullsGroup($byeCount, $championship->category->isTeam);
136
    }
137
138
    /**
139
     * @param $fighterCount
140
     *
141
     * @return int
142
     */
143
    private function getTreeSize($fighterCount, $groupSize)
144
    {
145
        $square = collect([1, 2, 4, 8, 16, 32, 64]);
146
        $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...
147
            return $item * $groupSize;
148
        });
149
150
        foreach ($squareMultiplied as $limit) {
151
            if ($fighterCount <= $limit) {
152
                return $limit;
153
            }
154
        }
155
156
        return 64 * $groupSize;
157
    }
158
159
    /**
160
     * @param $byeCount
161
     *
162
     * @return Collection
163
     */
164
    private function createNullsGroup($byeCount, $isTeam): Collection
165
    {
166
        $isTeam
167
            ? $null = new Team()
168
            : $null = new Competitor();
169
170
        $byeGroup = new Collection();
171
        for ($i = 0; $i < $byeCount; $i++) {
172
            $byeGroup->push($null);
173
        }
174
175
        return $byeGroup;
176
    }
177
178
    /**
179
     * @param $fighterGroups
180
     * @param int $max
181
     *
182
     * @return Collection
183
     */
184
    private function repart($fighterGroups, $max)
185
    {
186
        $fighters = new Collection();
187
        for ($i = 0; $i < $max; $i++) {
188
            foreach ($fighterGroups as $fighterGroup) {
189
                $fighter = $fighterGroup->values()->get($i);
190
                if ($fighter != null) {
191
                    $fighters->push($fighter);
192
                }
193
            }
194
        }
195
196
        return $fighters;
197
    }
198
199
    /**
200
     * Insert byes in an homogen way.
201
     *
202
     * @param Collection $fighters
203
     * @param Collection $byeGroup
204
     *
205
     * @return Collection
206
     */
207
    private function insertByes(Collection $fighters, Collection $byeGroup)
208
    {
209
        $bye = count($byeGroup) > 0 ? $byeGroup[0] : [];
210
        $sizeFighters = count($fighters);
211
        $sizeGroupBy = count($byeGroup);
212
213
        $frequency = $sizeGroupBy != 0
214
            ? (int)floor($sizeFighters / $sizeGroupBy)
215
            : -1;
216
217
        // Create Copy of $competitors
218
        $newFighters = new Collection();
219
        $i = 0;
220
        $byeCount = 0;
221
        foreach ($fighters as $fighter) {
222
            if ($frequency != -1 && $i % $frequency == 0 && $byeCount < $sizeGroupBy) {
223
                $newFighters->push($bye);
224
                $byeCount++;
225
            }
226
            $newFighters->push($fighter);
227
            $i++;
228
        }
229
230
        return $newFighters;
231
    }
232
233
    private function getFighters()
234
    {
235
        $this->championship->category->isTeam()
236
            ? $fighters = $this->championship->teams
237
            : $fighters = $this->championship->competitors;
238
239
        return $fighters;
240
    }
241
242
    /**
243
     * @param $usersByArea
244
     * @param $area
245
     *
246
     */
247
    public function generateGroupsForRound($usersByArea, $area, $round)
248
    {
249
        $previousRound = $this->getPreviousRound(1);
250
251
        foreach ($usersByArea as $fightersByEntity) {
252
            // Chunking to make small round robin groups
253
            if ($this->championship->hasPreliminary()) {
254
                $fightersGroup = $fightersByEntity->chunk($this->settings->preliminaryGroupSize)->shuffle();
255
            } elseif ($this->championship->isDirectEliminationType() || $round > 1) {
256
                $fightersGroup = $fightersByEntity->chunk(2)->shuffle();
257
            } else { // Round Robin
258
                $fightersGroup = $fightersByEntity->chunk($fightersByEntity->count());
259
            }
260
            $order = sizeof($fightersGroup);
261
            // Before doing anything, check last group if numUser = 1
262
            foreach ($fightersGroup->reverse() as $value => $fighters) {
263
                $parent = $this->getParentGroup($round, null, $value + 1, $previousRound);
264
                $group = $this->saveGroupAndSync($fighters, $area, $order, $round, $parent);
265
                $this->tree->push($group);
266
                $order--;
267
            }
268
            $area++;
269
        }
270
    }
271
272
    /**
273
     * @param $fighters
274
     * @param $area
275
     * @param $order
276
     * @param $round
277
     * @return FightersGroup
278
     */
279
    public function saveGroupAndSync($fighters, $area, $order, $round, $parent)
280
    {
281
282
        $fighters = $fighters->pluck('id')->shuffle();
283
284
        $group = $this->saveGroup($area, $order, $round, $parent);
285
286
        // Add all competitors to Pivot Table
287
        if ($this->championship->category->isTeam()) {
288
            $group->syncTeams($fighters);
289
        } else {
290
            $group->syncCompetitors($fighters);
291
        }
292
293
        return $group;
294
    }
295
296
    /**
297
     * @param $numFighters
298
     */
299
    private function pushEmptyGroupsToTree($numFighters)
300
    {
301
        $numFightersEliminatory = $numFighters;
302
        // We check what will be the number of groups after the preliminaries
303
        if ($this->championship->hasPreliminary()) {
304
            $numFightersEliminatory = $numFighters / $this->championship->getSettings()->preliminaryGroupSize * 2;
305
        }
306
        // We calculate how much rounds we will have
307
        $numRounds = intval(log($numFightersEliminatory, 2));
308
        $this->pushGroups($numRounds, $numFightersEliminatory);
309
    }
310
311
    /**
312
     * @param $area
313
     * @param $order
314
     * @param $round
315
     * @return FightersGroup
316
     */
317
    private function saveGroup($area, $order, $round, $parent): FightersGroup
318
    {
319
        $group = new FightersGroup();
320
        $group->area = $area;
321
        $group->order = $order;
322
        $group->round = $round;
323
        $group->championship_id = $this->championship->id;
324
        if ($parent != null) {
325
            $group->parent_id = $parent->id;
326
        }
327
        $group->save();
328
        return $group;
329
    }
330
331
    private function createByeFighter()
332
    {
333
        return $this->championship->category->isTeam
334
            ? new Team()
335
            : new Competitor();
336
    }
337
338
    public function createByeGroup($groupSize): Collection
339
    {
340
        $byeFighter = $this->createByeFighter();
341
        $group = new Collection();
342
        for ($i = 0; $i < $groupSize; $i++) {
343
            $group->push($byeFighter);
344
        }
345
        return $group;
346
    }
347
348
    /**
349
     * @param $fighters
350
     * @param $fighterGroups
351
     * @return Collection
352
     */
353
    private function adjustFightersGroupWithByes($fighters, $fighterGroups): Collection
354
    {
355
        $tmpFighterGroups = clone $fighterGroups;
356
357
        $byeGroup = $this->getByeGroup($this->championship, $fighters);
358
359
        // Get biggest competitor's group
360
        $max = $this->getMaxFightersByEntity($tmpFighterGroups);
361
362
        // We reacommodate them so that we can mix them up and they don't fight with another competitor of his entity.
363
364
        $fighters = $this->repart($fighterGroups, $max);
365
        $fighters = $this->insertByes($fighters, $byeGroup);
366
367
        return $fighters;
368
    }
369
370
    /**
371
     * @param $currentRound
372
     * @param $numRounds
373
     * @return Collection
374
     */
375
    private function getPreviousRound($currentRound, $numRounds = 0)
376
    {
377
        $previousRound = null;
378
        if ($currentRound != $numRounds) {
379
            $previousRound = $this->championship->groupsByRound($currentRound + 1);
380
        }
381
        return $previousRound;
382
    }
383
384
    /**
385
     * @param $roundNumber
386
     * @param $numRounds
387
     * @param $matchNumber
388
     * @param $previousRound
389
     * @return mixed
390
     */
391
    private function getParentGroup($roundNumber, $numRounds = 0, $matchNumber, $previousRound)
392
    {
393
        $parent = null;
394
        if ($roundNumber != $numRounds) {
395
            $parentIndex = intval(($matchNumber + 1) / 2);
396
            $parent = $previousRound->get($parentIndex - 1);
397
        }
398
        return $parent;
399
    }
400
401
    /**
402
     * @param $numRounds
403
     * @param $numFightersEliminatory
404
     */
405
    private function pushGroups($numRounds, $numFightersEliminatory)
406
    {
407
        for ($roundNumber = $numRounds; $roundNumber > 1; $roundNumber--) {
408
            $previousRound = $this->getPreviousRound($roundNumber, $numRounds);
409
            for ($matchNumber = ($numFightersEliminatory / pow(2, $roundNumber)); $matchNumber > 0; $matchNumber--) {
410
                $fighters = $this->createByeGroup(2);
411
                $parent = $this->getParentGroup($roundNumber, $numRounds, $matchNumber, $previousRound);
412
                $group = $this->saveGroupAndSync($fighters, $area = 1, $order = $matchNumber, $roundNumber, $parent);
413
                $this->tree->prepend($group);
414
            }
415
        }
416
    }
417
}
418