Completed
Push — master ( 17e0da...c0bdc3 )
by Julien
07:40
created

TreeGen::getFightersByArea()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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