Passed
Push — master ( de2a89...024236 )
by Julien
04:22
created

TreeGen::getNumRounds()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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