Completed
Push — master ( 11cee0...089394 )
by Julien
02:52
created

TreeGen::pushEmptyGroupsToTree()

Size

Total Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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