Completed
Push — master ( 1b67ee...4e860c )
by Julien
02:07
created

TreeGen::getTreeSize()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 24
rs 8.6845
cc 4
eloc 13
nc 4
nop 2
1
<?php
2
3
namespace Xoco70\KendoTournaments\TreeGen;
4
5
use Illuminate\Support\Collection;
6
use Illuminate\Support\Facades\App;
7
use Xoco70\KendoTournaments\Contracts\TreeGenerable;
8
use Xoco70\KendoTournaments\Exceptions\TreeGenerationException;
9
use Xoco70\KendoTournaments\Models\Championship;
10
use Xoco70\KendoTournaments\Models\ChampionshipSettings;
11
use Xoco70\KendoTournaments\Models\Fight;
12
use Xoco70\KendoTournaments\Models\FightersGroup;
13
14
abstract class TreeGen implements TreeGenerable
15
{
16
    protected $groupBy;
17
    protected $tree;
18
    public $championship;
19
    public $settings;
20
    protected $numFighters;
21
22
    abstract protected function pushEmptyGroupsToTree($numFighters);
23
24
    abstract protected function generateFights();
25
26
    abstract protected function createByeFighter();
27
28
    abstract protected function chunkAndShuffle($round, Collection $fightersByEntity);
29
30
    abstract protected function addFighterToGroup(FightersGroup $group, $fighter);
31
32
    abstract protected function syncGroup(FightersGroup $group, $fighters);
33
34
    abstract protected function getByeGroup($fighters);
35
36
    abstract protected function getFighter($fighterId);
37
38
    abstract protected function getFighters();
39
40
    abstract protected function getNumRounds($fightersCount);
41
42
    /**
43
     * @param Championship $championship
44
     * @param $groupBy
45
     */
46
    public function __construct(Championship $championship, $groupBy)
47
    {
48
        $this->championship = $championship;
49
        $this->groupBy = $groupBy;
50
        $this->settings = $championship->getSettings();
51
        $this->tree = new Collection();
52
    }
53
54
55
    /**
56
     * Generate tree groups for a championship.
57
     *
58
     * @throws TreeGenerationException
59
     */
60
    public function run()
61
    {
62
        $usersByArea = $this->getFightersByArea();
63
        $numFighters = sizeof($usersByArea->collapse());
64
65
        $this->generateGroupsForRound($usersByArea, 1);
66
        $this->pushEmptyGroupsToTree($numFighters);
67
        $this->addParentToChildren($numFighters);
68
        $this->generateFights();
69
        $this->generateNextRoundsFights();
70
        Fight::generateFightsId($this->championship);
71
72
    }
73
74
    /**
75
     * Get the biggest entity group
76
     * @param $userGroups
77
     *
78
     * @return int
79
     */
80
    private function getMaxFightersByEntity($userGroups): int
81
    {
82
        return $userGroups
83
            ->sortByDesc(function ($group) {
84
                return $group->count();
85
            })
86
            ->first()
87
            ->count();
88
89
    }
90
91
    /**
92
     * Get Competitor's list ordered by entities
93
     * Countries for Internation Tournament, State for a National Tournament, etc.
94
     *
95
     * @param $fighters
96
     * @return Collection
97
     */
98
    private function getFightersByEntity($fighters): Collection
99
    {
100
        // Right now, we are treating users and teams as equals.
101
        // It doesn't matter right now, because we only need name attribute which is common to both models
102
103
        // $this->groupBy contains federation_id, association_id, club_id, etc.
104
        if (($this->groupBy) != null) {
105
            $fighterGroups = $fighters->groupBy($this->groupBy); // Collection of Collection
106
        } else {
107
            $fighterGroups = $fighters->chunk(1); // Collection of Collection
108
        }
109
        return $fighterGroups;
110
    }
111
112
    /**
113
     * Get the size the first round will have
114
     * @param $fighterCount
115
     * @param $groupSize
116
     * @return int
117
     */
118
    protected function getTreeSize($fighterCount, $groupSize)
119
    {
120
        $squareMultiplied = collect([1, 2, 4, 8, 16, 32, 64])
121
            ->map(function ($item) use ($groupSize) {
122
                return $item * $groupSize;
123
            }); // [4, 8, 16, 32, 64,...]
0 ignored issues
show
Unused Code Comprehensibility introduced by
73% 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...
124
125
        foreach ($squareMultiplied as $limit) {
126
            if ($fighterCount <= $limit) {
127
                $treeSize = $limit;
128
                $numAreas = $this->championship->getSettings()->fightingAreas;
129
                $fighterCountPerArea = $treeSize / $numAreas;
130
                if ($fighterCountPerArea < $groupSize) {
131
                    $treeSize = $treeSize * $numAreas;
132
                }
133
                return $treeSize;
134
            }
135
136
        }
137
138
139
        return 64 * $groupSize;
140
141
    }
142
143
    /**
144
     * Repart BYE in the tree,
145
     * @param $fighterGroups
146
     * @param int $max
147
     *
148
     * @return Collection
149
     */
150
    private function repart($fighterGroups, $max)
151
    {
152
        $fighters = new Collection();
153
        for ($i = 0; $i < $max; $i++) {
154
            foreach ($fighterGroups as $fighterGroup) {
155
                $fighter = $fighterGroup->values()->get($i);
156
                if ($fighter != null) {
157
                    $fighters->push($fighter);
158
                }
159
            }
160
        }
161
162
        return $fighters;
163
    }
164
165
    /**
166
     * Insert byes in an homogen way.
167
     *
168
     * @param Collection $fighters
169
     * @param Collection $byeGroup
170
     *
171
     * @return Collection
172
     */
173
    private function insertByes(Collection $fighters, Collection $byeGroup)
174
    {
175
        $bye = count($byeGroup) > 0 ? $byeGroup[0] : [];
176
        $sizeFighters = count($fighters);
177
        $sizeGroupBy = count($byeGroup);
178
179
        $frequency = $sizeGroupBy != 0
180
            ? (int)floor($sizeFighters / $sizeGroupBy)
181
            : -1;
182
183
        // Create Copy of $competitors
184
        return $this->getFullFighterList($fighters, $frequency, $sizeGroupBy, $bye);
185
    }
186
187
    /**
188
     * @param Collection $usersByArea
189
     * @param integer $round
190
     *
191
     */
192
    public function generateGroupsForRound($usersByArea, $round)
193
    {
194
        $order = 1;
195
        foreach ($usersByArea as $fightersByEntity) {
196
            // Chunking to make small round robin groups
197
            $chunkedFighters = $this->chunkAndShuffle($round, $fightersByEntity);
198
            foreach ($chunkedFighters as $fighters) {
199
                $fighters = $fighters->pluck('id');
200
                if (!App::runningUnitTests()) {
201
                    $fighters = $fighters->shuffle();
202
                }
203
                $group = $this->saveGroup($order, $round, null);
204
                $this->syncGroup($group, $fighters);
205
                $order++;
206
            }
207
        }
208
    }
209
210
    /**
211
     * @param $order
212
     * @param $round
213
     * @param $parent
214
     * @return FightersGroup
215
     */
216
    protected function saveGroup($order, $round, $parent): FightersGroup
217
    {
218
        $group = new FightersGroup();
219
        $group->area = $this->getNumArea($round, $order);
220
        $group->order = $order;
221
        $group->round = $round;
222
        $group->championship_id = $this->championship->id;
223
        if ($parent != null) {
224
            $group->parent_id = $parent->id;
225
        }
226
        $group->save();
227
        return $group;
228
    }
229
230
231
    /**
232
     * @param integer $groupSize
233
     * @return Collection
234
     */
235
    public function createByeGroup($groupSize): Collection
236
    {
237
        $byeFighter = $this->createByeFighter();
238
        $group = new Collection();
239
        for ($i = 0; $i < $groupSize; $i++) {
240
            $group->push($byeFighter);
241
        }
242
        return $group;
243
    }
244
245
    /**
246
     * @param $fighters
247
     * @param Collection $fighterGroups
248
     * @return Collection
249
     */
250
    public function adjustFightersGroupWithByes($fighters, $fighterGroups): Collection
251
    {
252
        $tmpFighterGroups = clone $fighterGroups;
253
        $byeGroup = $this->getByeGroup($fighters);
254
255
        // Get biggest competitor's group
256
        $max = $this->getMaxFightersByEntity($tmpFighterGroups);
257
258
        // We reacommodate them so that we can mix them up and they don't fight with another competitor of his entity.
259
260
        $fighters = $this->repart($fighterGroups, $max);
261
        $fighters = $this->insertByes($fighters, $byeGroup);
262
263
        return $fighters;
264
    }
265
266
    /**
267
     * Get All Groups on previous round
268
     * @param $currentRound
269
     * @return Collection
270
     */
271
    private function getPreviousRound($currentRound)
272
    {
273
        $previousRound = $this->championship->groupsByRound($currentRound + 1)->get();
274
        return $previousRound;
275
    }
276
277
    /**
278
     * Get the next group on the right ( parent ), final round being the ancestor
279
     * @param $matchNumber
280
     * @param Collection $previousRound
281
     * @return mixed
282
     */
283
    private function getParentGroup($matchNumber, $previousRound)
284
    {
285
        $parentIndex = intval(($matchNumber + 1) / 2);
286
        $parent = $previousRound->get($parentIndex - 1);
287
        return $parent;
288
    }
289
290
291
    /**
292
     * Group Fighters by area
293
     * @return Collection
294
     * @throws TreeGenerationException
295
     */
296
    private function getFightersByArea()
297
    {
298
        // If previous trees already exists, delete all
299
        $this->championship->fightersGroups()->delete();
300
        $areas = $this->settings->fightingAreas;
301
        $fighters = $this->getFighters();
302
303
        // If there is less than 2 competitors average by area
304
        if ($fighters->count() / $areas < ChampionshipSettings::MIN_COMPETITORS_BY_AREA) {
305
            throw new TreeGenerationException(trans('msg.min_competitor_required', ['number' => config('kendo-tournaments.MIN_COMPETITORS_X_AREA')]));
306
        }
307
308
        if ($this->settings->hasPreliminary && $fighters->count() / ($this->settings->preliminaryGroupSize * $areas) < 1 ) {
309
            throw new TreeGenerationException(trans('msg.min_competitor_required', ['number' => config('kendo-tournaments.MIN_COMPETITORS_X_AREA')]));
310
        }
311
312
        // Get Competitor's / Team list ordered by entities ( Federation, Assoc, Club, etc...)
313
        $fighterByEntity = $this->getFightersByEntity($fighters); // Chunk(1)
314
        $fightersWithBye = $this->adjustFightersGroupWithByes($fighters, $fighterByEntity);
315
316
        // Chunk user by areas
317
        return $fightersWithBye->chunk(count($fightersWithBye) / $areas);
318
    }
319
320
    /**
321
     * Attach a parent to every child for nestedSet Navigation
322
     * @param $numFightersElim
323
     */
324
    private function addParentToChildren($numFightersElim)
325
    {
326
        $numRounds = $this->getNumRounds($numFightersElim);
327
        $groupsDesc = $this->championship
328
            ->fightersGroups()
329
            ->where('round', '<', $numRounds)
330
            ->orderByDesc('id')->get();
331
332
        $groupsDescByRound = $groupsDesc->groupBy('round');
333
334
        foreach ($groupsDescByRound as $round => $groups) {
335
            $previousRound = $this->getPreviousRound($round);
336
            foreach ($groups->reverse()->values() as $matchNumber => $group) {
337
                $parent = $this->getParentGroup($matchNumber + 1, $previousRound);
338
                $group->parent_id = $parent->id;
339
                $group->save();
340
            }
341
        }
342
    }
343
344
    /**
345
     * @param Collection $fighters
346
     * @param $frequency
347
     * @param $sizeGroupBy
348
     * @param $bye
349
     * @return Collection
350
     */
351
    private function getFullFighterList(Collection $fighters, $frequency, $sizeGroupBy, $bye): Collection
352
    {
353
        $newFighters = new Collection();
354
        $count = 0;
355
        $byeCount = 0;
356
        foreach ($fighters as $fighter) {
357
            if ($this->shouldInsertBye($frequency, $sizeGroupBy, $count, $byeCount)) {
358
                $newFighters->push($bye);
359
                $byeCount++;
360
            }
361
            $newFighters->push($fighter);
362
            $count++;
363
        }
364
        return $newFighters;
365
    }
366
367
    /**
368
     * @param $frequency
369
     * @param $sizeGroupBy
370
     * @param $count
371
     * @param $byeCount
372
     * @return bool
373
     */
374
    private function shouldInsertBye($frequency, $sizeGroupBy, $count, $byeCount): bool
375
    {
376
        return $frequency != -1 && $count % $frequency == 0 && $byeCount < $sizeGroupBy;
377
    }
378
379
380
    /**
381
     * Destroy Previous Fights for demo
382
     */
383
    protected function destroyPreviousFights()
384
    {
385
        // Delete previous fight for this championship
386
        $arrGroupsId = $this->championship->fightersGroups()->get()->pluck('id');
387
        Fight::destroy($arrGroupsId);
388
    }
389
390
391
    /**
392
     * Generate Fights for next rounds
393
     */
394
    public function generateNextRoundsFights()
395
    {
396
        $fightersCount = $this->championship->competitors->count() + $this->championship->teams->count();
397
        $maxRounds = $this->getNumRounds($fightersCount);
398
        for ($numRound = 1; $numRound < $maxRounds; $numRound++) {
399
            $groupsByRound = $this->championship->fightersGroups()->where('round', $numRound)->with('parent', 'children')->get();
400
            $this->updateParentFight($groupsByRound); // should be groupsByRound
401
        }
402
    }
403
404
    /**
405
     * @param $groupsByRound
406
     */
407
    protected function updateParentFight($groupsByRound)
408
    {
409
        foreach ($groupsByRound as $keyGroup => $group) {
410
            $parentGroup = $group->parent;
411
            if ($parentGroup == null) break;
412
            $parentFight = $parentGroup->fights->get(0);
413
414
            // determine whether c1 or c2 must be updated
415
            $this->chooseAndUpdateParentFight($keyGroup, $group, $parentFight);
416
        }
417
    }
418
419
    /**
420
     * @param $group
421
     * @param $parentFight
422
     */
423
    protected function chooseAndUpdateParentFight($keyGroup, FightersGroup $group, Fight $parentFight)
424
    {
425
        // we need to know if the child has empty fighters, is this BYE or undetermined
426
        if ($group->hasDeterminedParent()) {
427
            $valueToUpdate = $group->getValueToUpdate(); // This should be OK
428
            if ($valueToUpdate != null) {
429
                $fighterToUpdate = $group->getParentFighterToUpdate($keyGroup);
430
                $parentFight->$fighterToUpdate = $valueToUpdate;
431
                $parentFight->save();
432
                // Add fighter to pivot table
433
                $parentGroup = $parentFight->group;
434
435
                $fighter = $this->getFighter($valueToUpdate);
436
                $this->addFighterToGroup($parentGroup, $fighter);
437
            }
438
        }
439
    }
440
441
442
    /**
443
     * Calculate the area of the group ( group is still not created )
444
     * @param $round
445
     * @param $order
446
     * @return int
447
     */
448
    protected function getNumArea($round, $order)
449
    {
450
        $totalAreas = $this->settings->fightingAreas;
451
        $numFighters = $this->championship->fighters->count(); // 4
452
        $numGroups = $this->getTreeSize($numFighters, $this->championship->getGroupSize()) / $this->championship->getGroupSize(); // 1 -> 1
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% 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...
453
454
        $areaSize = $numGroups / ($totalAreas * pow(2, $round - 1));
455
456
        $numArea = intval(ceil($order / $areaSize)); // if round == 4, and second match 2/2 = 1 BAD
457
//        dump($numArea);
458
        return $numArea;
459
    }
460
}
461